From 8d5bc315c66d76f5628a12e9af5ae51e776d7863 Mon Sep 17 00:00:00 2001 From: Thomas Fradet <t.fradet8@gmail.com> Date: Thu, 14 Feb 2019 14:10:13 +0100 Subject: [PATCH] folder --- activity_modules/block_activity_modules.php | 106 +++ activity_modules/classes/privacy/provider.php | 46 ++ activity_modules/db/access.php | 41 + .../lang/en/block_activity_modules.php | 27 + .../behat/block_activity_modules.feature | 159 ++++ activity_modules/version.php | 29 + ...tore_activity_results_block_task.class.php | 115 +++ activity_results/block_activity_results.php | 707 ++++++++++++++++ activity_results/classes/privacy/provider.php | 46 ++ activity_results/db/access.php | 41 + activity_results/edit_form.php | 127 +++ .../lang/en/block_activity_results.php | 68 ++ activity_results/settings.php | 86 ++ activity_results/styles.css | 28 + .../tests/behat/addblockinactivity.feature | 102 +++ .../tests/behat/addunconfiguredblock.feature | 42 + .../behat/addunsupportedactivity.feature | 39 + .../tests/behat/defaultsettings.feature | 64 ++ .../behat/highscoreswithoutgroups.feature | 169 ++++ .../tests/behat/highscoreswithscales.feature | 109 +++ .../highscoreswithscalesandgroups.feature | 151 ++++ .../highscoreswithseperategroups.feature | 227 +++++ .../behat/highscoreswithvisiblegroups.feature | 204 +++++ .../behat/lowscoreswithoutgroups.feature | 158 ++++ .../tests/behat/lowscoreswithscales.feature | 110 +++ .../lowscoreswithscalesandgroups.feature | 147 ++++ .../behat/lowscoreswithseperategroups.feature | 219 +++++ .../behat/lowscoreswithvisiblegroups.feature | 200 +++++ activity_results/version.php | 29 + admin_bookmarks/block_admin_bookmarks.php | 138 ++++ admin_bookmarks/classes/privacy/provider.php | 46 ++ admin_bookmarks/create.php | 71 ++ admin_bookmarks/db/access.php | 51 ++ admin_bookmarks/delete.php | 73 ++ .../lang/en/block_admin_bookmarks.php | 28 + .../tests/behat/bookmark_admin_pages.feature | 36 + admin_bookmarks/version.php | 29 + badges/block_badges.php | 108 +++ badges/classes/privacy/provider.php | 46 ++ badges/db/access.php | 45 + badges/db/upgrade.php | 58 ++ badges/edit_form.php | 38 + badges/lang/en/block_badges.php | 31 + badges/tests/behat/block_badges.feature | 32 + .../tests/behat/block_badges_course.feature | 71 ++ .../behat/block_badges_dashboard.feature | 38 + .../behat/block_badges_frontpage.feature | 44 + badges/tests/fixtures/badge.png | Bin 0 -> 2116 bytes badges/version.php | 30 + blog_menu/block_blog_menu.php | 123 +++ blog_menu/classes/privacy/provider.php | 46 ++ blog_menu/db/access.php | 41 + blog_menu/lang/en/block_blog_menu.php | 28 + blog_menu/tests/behat/block_blog_menu.feature | 76 ++ .../behat/block_blog_menu_activity.feature | 213 +++++ .../behat/block_blog_menu_course.feature | 190 +++++ .../behat/block_blog_menu_frontpage.feature | 30 + blog_menu/version.php | 29 + blog_recent/block_blog_recent.php | 128 +++ blog_recent/classes/privacy/provider.php | 46 ++ blog_recent/db/access.php | 41 + blog_recent/edit_form.php | 61 ++ blog_recent/lang/en/block_blog_recent.php | 31 + .../tests/behat/block_blog_recent.feature | 32 + .../behat/block_blog_recent_activity.feature | 115 +++ .../behat/block_blog_recent_course.feature | 105 +++ .../behat/block_blog_recent_frontpage.feature | 98 +++ blog_recent/version.php | 29 + blog_tags/block_blog_tags.php | 229 ++++++ blog_tags/classes/privacy/provider.php | 46 ++ blog_tags/db/access.php | 41 + blog_tags/edit_form.php | 66 ++ blog_tags/lang/en/block_blog_tags.php | 28 + blog_tags/styles.css | 68 ++ blog_tags/tests/behat/blogtag.feature | 55 ++ blog_tags/version.php | 29 + calendar_month/block_calendar_month.php | 65 ++ calendar_month/classes/privacy/provider.php | 46 ++ calendar_month/db/access.php | 51 ++ calendar_month/db/upgrade.php | 58 ++ .../lang/en/block_calendar_month.php | 28 + .../tests/behat/block_calendar_month.feature | 195 +++++ .../behat/block_calendar_month_course.feature | 27 + .../block_calendar_month_dashboard.feature | 19 + .../block_calendar_month_frontpage.feature | 23 + calendar_month/version.php | 29 + calendar_upcoming/block_calendar_upcoming.php | 127 +++ .../classes/privacy/provider.php | 46 ++ calendar_upcoming/db/access.php | 51 ++ calendar_upcoming/db/upgrade.php | 58 ++ .../lang/en/block_calendar_upcoming.php | 29 + .../block_calendar_upcoming_course.feature | 26 + .../block_calendar_upcoming_dashboard.feature | 19 + .../block_calendar_upcoming_frontpage.feature | 24 + calendar_upcoming/upgrade.txt | 5 + calendar_upcoming/version.php | 29 + career/README.md | 49 ++ career/block_career.php | 109 +++ career/career_list.php | 38 + career/career_setting.php | 82 ++ career/career_unit.php | 30 + career/db/access.php | 25 + career/db/install.xml | 21 + career/edit_form.php | 20 + career/entity/block_career_ressource.php | 108 +++ career/entity/block_career_section.php | 91 ++ ...\251cran 2018-06-19 \303\240 18.18.32.png" | Bin 0 -> 176862 bytes career/img/file.txt | 0 career/img/numoc-16-9-blanc.png | Bin 0 -> 86910 bytes career/img/rose-rose.jpg | Bin 0 -> 45517 bytes career/index.php | 12 + career/js/file.js | 48 ++ career/js/interact.min.js | 5 + career/js/jquery-1.8.2.min.js | 2 + career/js/jquery.min.js | 4 + career/lang/en/block_career.php | 18 + career/lang/fr/block_career.php | 17 + career/styles.css | 260 ++++++ career/version.php | 35 + career/view/view_career_list.php | 55 ++ career/view/view_career_setting.php | 196 +++++ career/view/view_career_unit.php | 106 +++ classes 14.04.12/external.php | 137 +++ classes 14.04.12/privacy/provider.php | 227 +++++ comments/block_comments.php | 88 ++ comments/classes/event/comment_created.php | 38 + comments/classes/event/comment_deleted.php | 38 + comments/classes/privacy/provider.php | 115 +++ comments/db/access.php | 51 ++ comments/lang/en/block_comments.php | 29 + comments/lib.php | 83 ++ comments/tests/behat/add_comment.feature | 98 +++ comments/tests/behat/behat_block_comments.php | 110 +++ .../behat/block_comment_activity.feature | 33 + .../tests/behat/block_comment_course.feature | 28 + .../behat/block_comment_dashboard.feature | 29 + .../behat/block_comment_frontpage.feature | 21 + comments/tests/behat/delete_comment.feature | 34 + comments/tests/events_test.php | 184 +++++ comments/tests/privacy_provider_test.php | 468 +++++++++++ comments/version.php | 29 + community/block_community.php | 104 +++ community/classes/privacy/provider.php | 181 ++++ community/communitycourse.php | 208 +++++ community/db/access.php | 51 ++ community/db/install.xml | 21 + community/db/upgrade.php | 59 ++ community/forms.php | 173 ++++ community/lang/en/block_community.php | 121 +++ community/locallib.php | 88 ++ community/renderer.php | 400 +++++++++ community/styles.css | 355 ++++++++ community/tests/privacy_test.php | 267 ++++++ community/version.php | 29 + community/yui/comments/comments.js | 97 +++ community/yui/imagegallery/imagegallery.js | 206 +++++ completionstatus/block_completionstatus.php | 256 ++++++ completionstatus/classes/privacy/provider.php | 46 ++ completionstatus/db/access.php | 41 + completionstatus/db/upgrade.php | 61 ++ completionstatus/details.php | 263 ++++++ .../lang/en/block_completionstatus.php | 32 + .../behat/block_completionstatus.feature | 54 ++ ...mpletionstatus_activity_completion.feature | 70 ++ ...lock_completionstatus_manual_other.feature | 103 +++ ...block_completionstatus_manual_self.feature | 45 + completionstatus/version.php | 31 + course_list/block_course_list.php | 177 ++++ course_list/classes/privacy/provider.php | 46 ++ course_list/db/access.php | 51 ++ course_list/lang/en/block_course_list.php | 34 + course_list/settings.php | 37 + course_list/styles.css | 7 + .../behat/block_course_list_category.feature | 78 ++ .../behat/block_course_list_course.feature | 87 ++ .../behat/block_course_list_dashboard.feature | 61 ++ .../behat/block_course_list_frontpage.feature | 86 ++ course_list/version.php | 29 + course_summary/block_course_summary.php | 79 ++ course_summary/classes/privacy/provider.php | 46 ++ course_summary/db/access.php | 41 + course_summary/db/upgrade.php | 61 ++ .../lang/en/block_course_summary.php | 29 + course_summary/styles.css | 7 + .../behat/block_course_summary_course.feature | 36 + .../block_course_summary_frontpage.feature | 30 + course_summary/version.php | 29 + edit_form 14.04.12.php | 313 +++++++ feedback/block_feedback.php | 73 ++ feedback/classes/privacy/provider.php | 46 ++ feedback/db/access.php | 41 + feedback/db/install.php | 29 + feedback/lang/en/block_feedback.php | 28 + feedback/version.php | 31 + globalsearch/block_globalsearch.php | 96 +++ globalsearch/classes/privacy/provider.php | 46 ++ globalsearch/db/access.php | 48 ++ globalsearch/lang/en/block_globalsearch.php | 28 + globalsearch/styles.css | 7 + globalsearch/version.php | 30 + ...store_glossary_random_block_task.class.php | 95 +++ glossary_random/block_glossary_random.php | 258 ++++++ glossary_random/classes/privacy/provider.php | 46 ++ glossary_random/db/access.php | 51 ++ glossary_random/edit_form.php | 79 ++ .../lang/en/block_glossary_random.php | 48 ++ .../tests/behat/glossary_random.feature | 108 +++ .../behat/glossary_random_frontpage.feature | 27 + .../behat/glossary_random_global.feature | 79 ++ glossary_random/version.php | 31 + html/backup/moodle1/lib.php | 64 ++ .../moodle2/backup_html_block_task.class.php | 50 ++ .../moodle2/restore_html_block_task.class.php | 93 +++ html/block_html.php | 177 ++++ html/classes/privacy/provider.php | 196 +++++ html/classes/search/content.php | 91 ++ html/db/access.php | 51 ++ html/db/upgrade.php | 46 ++ html/edit_form.php | 91 ++ html/lang/en/block_html.php | 36 + html/lib.php | 112 +++ html/settings.php | 32 + .../behat/configuring_html_block.feature | 41 + html/tests/behat/course_block.feature | 35 + html/tests/behat/multiple_instances.feature | 42 + html/tests/privacy_provider_test.php | 344 ++++++++ html/tests/search_content_test.php | 191 +++++ html/version.php | 29 + index.html | 1 + login/block_login.php | 126 +++ login/classes/privacy/provider.php | 46 ++ login/db/access.php | 41 + login/lang/en/block_login.php | 27 + login/tests/behat/login_block.feature | 28 + login/version.php | 29 + lp/block_lp.php | 84 ++ .../output/competencies_to_review_page.php | 87 ++ lp/classes/output/plans_to_review_page.php | 82 ++ lp/classes/output/renderer.php | 70 ++ lp/classes/output/summary.php | 156 ++++ lp/classes/privacy/provider.php | 46 ++ lp/competencies_to_review.php | 48 ++ lp/db/access.php | 57 ++ lp/lang/en/block_lp.php | 37 + lp/plans_to_review.php | 48 ++ lp/styles.css | 17 + .../competencies_to_review_page.mustache | 49 ++ lp/templates/plans_to_review_page.mustache | 49 ++ lp/templates/summary.mustache | 87 ++ lp/version.php | 32 + mahara_iena | 1 + mentees/block_mentees.php | 82 ++ mentees/classes/privacy/provider.php | 46 ++ mentees/db/access.php | 51 ++ mentees/edit_form.php | 39 + mentees/lang/en/block_mentees.php | 32 + .../behat/configuring_mentees_block.feature | 18 + mentees/version.php | 29 + mnet_hosts/block_mnet_hosts.php | 156 ++++ mnet_hosts/classes/privacy/provider.php | 46 ++ mnet_hosts/db/access.php | 51 ++ mnet_hosts/lang/en/block_mnet_hosts.php | 32 + mnet_hosts/version.php | 29 + moodleblock.class.php | 778 ++++++++++++++++++ .../build/calendar_events_repository.min.js | 1 + myoverview/amd/build/event_list.min.js | 1 + .../amd/build/event_list_by_course.min.js | 1 + myoverview/amd/build/paging_bar.min.js | 1 + myoverview/amd/build/paging_content.min.js | 1 + myoverview/amd/build/tab_preferences.min.js | 1 + .../amd/src/calendar_events_repository.js | 168 ++++ myoverview/amd/src/event_list.js | 414 ++++++++++ myoverview/amd/src/event_list_by_course.js | 108 +++ myoverview/amd/src/paging_bar.js | 102 +++ myoverview/amd/src/paging_content.js | 105 +++ myoverview/amd/src/tab_preferences.js | 61 ++ myoverview/block_myoverview.php | 89 ++ myoverview/classes/output/courses_view.php | 190 +++++ myoverview/classes/output/main.php | 111 +++ myoverview/classes/output/renderer.php | 48 ++ myoverview/classes/privacy/provider.php | 61 ++ myoverview/db/access.php | 50 ++ myoverview/lang/en/block_myoverview.php | 47 ++ myoverview/lib.php | 52 ++ myoverview/pix/activities.svg | 41 + myoverview/pix/courses.svg | 52 ++ myoverview/settings.php | 39 + .../templates/course-event-list-item.mustache | 69 ++ .../course-event-list-items.mustache | 63 ++ .../templates/course-event-list.mustache | 110 +++ myoverview/templates/course-item.mustache | 44 + .../course-paging-content-item.mustache | 47 ++ .../templates/course-paging-content.mustache | 48 ++ myoverview/templates/course-summary.mustache | 49 ++ .../templates/courses-view-by-status.mustache | 45 + .../courses-view-course-item.mustache | 49 ++ myoverview/templates/courses-view.mustache | 115 +++ .../templates/event-list-group.mustache | 75 ++ myoverview/templates/event-list-item.mustache | 76 ++ .../templates/event-list-items.mustache | 68 ++ myoverview/templates/event-list.mustache | 87 ++ myoverview/templates/main.mustache | 55 ++ myoverview/templates/paging-bar-item.mustache | 41 + myoverview/templates/paging-bar.mustache | 96 +++ .../templates/paging-content-item.mustache | 36 + myoverview/templates/paging-content.mustache | 44 + myoverview/templates/progress-chart.mustache | 50 ++ .../templates/timeline-view-courses.mustache | 121 +++ .../templates/timeline-view-dates.mustache | 35 + myoverview/templates/timeline-view.mustache | 49 ++ .../behat/block_myoverview_dashboard.feature | 72 ++ .../behat/block_myoverview_progress.feature | 63 ++ myoverview/tests/privacy_test.php | 80 ++ myoverview/version.php | 29 + myprofile/block_myprofile.php | 238 ++++++ myprofile/classes/privacy/provider.php | 46 ++ myprofile/db/access.php | 51 ++ myprofile/edit_form.php | 151 ++++ myprofile/lang/en/block_myprofile.php | 51 ++ myprofile/lang/en/deprecated.txt | 1 + myprofile/styles.css | 14 + myprofile/tests/behat/block_myprofile.feature | 309 +++++++ .../behat/block_myprofile_activity.feature | 24 + .../behat/block_myprofile_course.feature | 20 + .../behat/block_myprofile_dashboard.feature | 14 + .../behat/block_myprofile_frontpage.feature | 25 + myprofile/version.php | 30 + .../amd/build/ajax_response_renderer.min.js | 1 + navigation/amd/build/nav_loader.min.js | 1 + navigation/amd/build/navblock.min.js | 1 + navigation/amd/build/site_admin_loader.min.js | 1 + navigation/amd/src/ajax_response_renderer.js | 165 ++++ navigation/amd/src/nav_loader.js | 64 ++ navigation/amd/src/navblock.js | 46 ++ navigation/amd/src/site_admin_loader.js | 52 ++ navigation/block_navigation.php | 336 ++++++++ navigation/classes/privacy/provider.php | 46 ++ navigation/db/access.php | 51 ++ navigation/db/upgrade.php | 68 ++ navigation/edit_form.php | 71 ++ navigation/lang/en/block_navigation.php | 42 + navigation/renderer.php | 192 +++++ navigation/styles.css | 76 ++ .../tests/behat/expand_courses_node.feature | 202 +++++ .../tests/behat/participants_link.feature | 54 ++ .../tests/behat/view_my_courses.feature | 104 +++ navigation/version.php | 29 + news_items/block_news_items.php | 156 ++++ news_items/classes/privacy/provider.php | 46 ++ news_items/db/access.php | 51 ++ news_items/lang/en/block_news_items.php | 28 + news_items/tests/behat/display_news.feature | 47 ++ news_items/version.php | 30 + online_users/block_online_users.php | 147 ++++ online_users/classes/fetcher.php | 165 ++++ online_users/classes/privacy/provider.php | 46 ++ online_users/db/access.php | 65 ++ online_users/lang/en/block_online_users.php | 36 + online_users/settings.php | 31 + online_users/styles.css | 21 + .../behat/block_online_users_course.feature | 41 + .../block_online_users_dashboard.feature | 28 + .../block_online_users_frontpage.feature | 51 ++ online_users/tests/generator/lib.php | 90 ++ online_users/tests/generator_test.php | 57 ++ online_users/tests/online_users_test.php | 151 ++++ online_users/version.php | 29 + participants/block_participants.php | 85 ++ participants/classes/privacy/provider.php | 46 ++ participants/db/access.php | 41 + participants/lang/en/block_participants.php | 27 + .../behat/block_participants_course.feature | 40 + .../block_participants_frontpage.feature | 26 + participants/version.php | 29 + private_files/block_private_files.php | 72 ++ private_files/classes/privacy/provider.php | 46 ++ private_files/db/access.php | 51 ++ private_files/edit.php | 29 + private_files/lang/en/block_private_files.php | 29 + private_files/module.js | 41 + private_files/renderer.php | 89 ++ private_files/styles.css | 10 + .../block_private_files_activity.feature | 28 + .../behat/block_private_files_course.feature | 24 + .../block_private_files_dashboard.feature | 17 + .../block_private_files_frontpage.feature | 34 + private_files/tests/fixtures/testfile.txt | 1 + private_files/version.php | 29 + .../restore_quiz_results_block_task.class.php | 113 +++ quiz_results/block_quiz_results.php | 61 ++ quiz_results/classes/privacy/provider.php | 46 ++ quiz_results/db/access.php | 41 + quiz_results/db/install.php | 31 + quiz_results/db/upgrade.php | 58 ++ quiz_results/lang/en/block_quiz_results.php | 27 + quiz_results/lang/en/depreciated.txt | 0 quiz_results/version.php | 31 + recent_activity/block_recent_activity.php | 309 +++++++ recent_activity/classes/observer.php | 73 ++ recent_activity/classes/privacy/provider.php | 97 +++ recent_activity/db/access.php | 57 ++ recent_activity/db/events.php | 47 ++ recent_activity/db/install.xml | 25 + recent_activity/db/upgrade.php | 60 ++ .../lang/en/block_recent_activity.php | 37 + recent_activity/renderer.php | 128 +++ recent_activity/styles.css | 12 + .../tests/behat/structural_changes.feature | 215 +++++ recent_activity/version.php | 30 + rss_client/backup/moodle1/lib.php | 48 ++ .../backup_rss_client_block_task.class.php | 54 ++ .../moodle2/backup_rss_client_stepslib.php | 83 ++ .../restore_rss_client_block_task.class.php | 58 ++ .../moodle2/restore_rss_client_stepslib.php | 90 ++ rss_client/block_rss_client.php | 387 +++++++++ rss_client/classes/output/block.php | 104 +++ rss_client/classes/output/channel_image.php | 151 ++++ rss_client/classes/output/feed.php | 224 +++++ rss_client/classes/output/footer.php | 111 +++ rss_client/classes/output/item.php | 286 +++++++ rss_client/classes/output/renderer.php | 121 +++ rss_client/classes/privacy/provider.php | 152 ++++ rss_client/db/access.php | 76 ++ rss_client/db/install.xml | 24 + rss_client/db/upgrade.php | 46 ++ rss_client/edit_form.php | 93 +++ rss_client/editfeed.php | 232 ++++++ rss_client/lang/en/block_rss_client.php | 90 ++ rss_client/managefeeds.php | 147 ++++ rss_client/settings.php | 36 + rss_client/styles.css | 10 + rss_client/templates/block.mustache | 91 ++ rss_client/templates/channel_image.mustache | 50 ++ rss_client/templates/feed.mustache | 79 ++ rss_client/templates/footer.mustache | 42 + rss_client/templates/item.mustache | 63 ++ rss_client/tests/cron_test.php | 149 ++++ rss_client/tests/privacy_test.php | 148 ++++ rss_client/version.php | 30 + rss_client/viewfeed.php | 99 +++ search_forums/block_search_forums.php | 66 ++ search_forums/classes/output/renderer.php | 50 ++ search_forums/classes/output/search_form.php | 74 ++ search_forums/classes/privacy/provider.php | 46 ++ search_forums/db/access.php | 41 + search_forums/lang/en/block_search_forums.php | 28 + search_forums/styles.css | 16 + search_forums/templates/search_form.mustache | 15 + .../behat/block_search_forums_course.feature | 71 ++ .../block_search_forums_frontpage.feature | 31 + search_forums/version.php | 31 + section_links/block_section_links.php | 159 ++++ section_links/classes/privacy/provider.php | 46 ++ section_links/db/access.php | 41 + section_links/db/upgrade.php | 62 ++ section_links/edit_form.php | 86 ++ section_links/lang/en/block_section_links.php | 39 + section_links/renderer.php | 76 ++ section_links/settings.php | 51 ++ .../behat/block_section_links_course.feature | 57 ++ section_links/version.php | 29 + selfcompletion/block_selfcompletion.php | 112 +++ selfcompletion/classes/privacy/provider.php | 46 ++ selfcompletion/db/access.php | 41 + selfcompletion/db/upgrade.php | 61 ++ .../lang/en/block_selfcompletion.php | 30 + selfcompletion/version.php | 29 + settings/amd/build/settingsblock.min.js | 1 + settings/amd/src/settingsblock.js | 51 ++ settings/block_settings.php | 163 ++++ settings/classes/privacy/provider.php | 46 ++ settings/db/access.php | 51 ++ settings/db/upgrade.php | 68 ++ settings/edit_form.php | 46 ++ settings/lang/en/block_settings.php | 31 + settings/renderer.php | 156 ++++ settings/styles.css | 65 ++ settings/version.php | 29 + site_main_menu/block_site_main_menu.php | 163 ++++ site_main_menu/classes/privacy/provider.php | 46 ++ site_main_menu/db/access.php | 41 + .../lang/en/block_site_main_menu.php | 28 + site_main_menu/styles.css | 34 + site_main_menu/tests/behat/add_url.feature | 19 + .../behat/behat_block_site_main_menu.php | 157 ++++ .../tests/behat/edit_activities.feature | 67 ++ site_main_menu/version.php | 29 + social_activities/block_social_activities.php | 153 ++++ .../classes/privacy/provider.php | 46 ++ social_activities/db/access.php | 41 + .../lang/en/block_social_activities.php | 27 + social_activities/styles.css | 16 + .../behat/behat_block_social_activities.php | 157 ++++ .../tests/behat/edit_activities.feature | 85 ++ social_activities/version.php | 29 + tag_flickr/block_tag_flickr.php | 183 ++++ tag_flickr/classes/privacy/provider.php | 94 +++ tag_flickr/db/access.php | 41 + tag_flickr/edit_form.php | 59 ++ tag_flickr/lang/en/block_tag_flickr.php | 41 + tag_flickr/styles.css | 3 + .../configuring_tag_flickr_block.feature | 20 + tag_flickr/version.php | 29 + tag_youtube/block_tag_youtube.php | 402 +++++++++ tag_youtube/classes/privacy/provider.php | 46 ++ tag_youtube/db/access.php | 41 + tag_youtube/db/install.php | 36 + tag_youtube/edit_form.php | 48 ++ tag_youtube/lang/en/block_tag_youtube.php | 50 ++ tag_youtube/settings.php | 30 + tag_youtube/styles.css | 10 + tag_youtube/tests/block_tag_youtube_test.php | 51 ++ tag_youtube/upgrade.txt | 8 + tag_youtube/version.php | 29 + .../moodle2/restore_tags_block_task.class.php | 88 ++ tags/block_tags.php | 112 +++ tags/classes/privacy/provider.php | 46 ++ tags/db/access.php | 51 ++ tags/edit_form.php | 100 +++ tags/lang/en/block_tags.php | 41 + tags/tests/behat/tagcloud.feature | 49 ++ tags/version.php | 29 + tests/behat/add_blocks.feature | 27 + tests/behat/behat_blocks.php | 152 ++++ .../configure_block_throughout_site.feature | 73 ++ tests/behat/hidden_block_region.feature | 52 ++ tests/behat/hide_blocks.feature | 27 + tests/behat/manage_blocks.feature | 60 ++ tests/behat/move_blocks.feature | 46 ++ tests/behat/restrict_available_blocks.feature | 38 + .../behat/return_block_original_state.feature | 47 ++ tests/externallib_test.php | 141 ++++ tests/privacy_test.php | 364 ++++++++ upgrade 14.04.12.txt | 82 ++ 534 files changed, 40311 insertions(+) create mode 100644 activity_modules/block_activity_modules.php create mode 100644 activity_modules/classes/privacy/provider.php create mode 100644 activity_modules/db/access.php create mode 100644 activity_modules/lang/en/block_activity_modules.php create mode 100644 activity_modules/tests/behat/block_activity_modules.feature create mode 100644 activity_modules/version.php create mode 100644 activity_results/backup/moodle2/restore_activity_results_block_task.class.php create mode 100644 activity_results/block_activity_results.php create mode 100644 activity_results/classes/privacy/provider.php create mode 100644 activity_results/db/access.php create mode 100644 activity_results/edit_form.php create mode 100644 activity_results/lang/en/block_activity_results.php create mode 100644 activity_results/settings.php create mode 100644 activity_results/styles.css create mode 100644 activity_results/tests/behat/addblockinactivity.feature create mode 100644 activity_results/tests/behat/addunconfiguredblock.feature create mode 100644 activity_results/tests/behat/addunsupportedactivity.feature create mode 100644 activity_results/tests/behat/defaultsettings.feature create mode 100644 activity_results/tests/behat/highscoreswithoutgroups.feature create mode 100644 activity_results/tests/behat/highscoreswithscales.feature create mode 100644 activity_results/tests/behat/highscoreswithscalesandgroups.feature create mode 100644 activity_results/tests/behat/highscoreswithseperategroups.feature create mode 100644 activity_results/tests/behat/highscoreswithvisiblegroups.feature create mode 100644 activity_results/tests/behat/lowscoreswithoutgroups.feature create mode 100644 activity_results/tests/behat/lowscoreswithscales.feature create mode 100644 activity_results/tests/behat/lowscoreswithscalesandgroups.feature create mode 100644 activity_results/tests/behat/lowscoreswithseperategroups.feature create mode 100644 activity_results/tests/behat/lowscoreswithvisiblegroups.feature create mode 100644 activity_results/version.php create mode 100644 admin_bookmarks/block_admin_bookmarks.php create mode 100644 admin_bookmarks/classes/privacy/provider.php create mode 100644 admin_bookmarks/create.php create mode 100644 admin_bookmarks/db/access.php create mode 100644 admin_bookmarks/delete.php create mode 100644 admin_bookmarks/lang/en/block_admin_bookmarks.php create mode 100644 admin_bookmarks/tests/behat/bookmark_admin_pages.feature create mode 100644 admin_bookmarks/version.php create mode 100644 badges/block_badges.php create mode 100644 badges/classes/privacy/provider.php create mode 100644 badges/db/access.php create mode 100644 badges/db/upgrade.php create mode 100644 badges/edit_form.php create mode 100644 badges/lang/en/block_badges.php create mode 100644 badges/tests/behat/block_badges.feature create mode 100644 badges/tests/behat/block_badges_course.feature create mode 100644 badges/tests/behat/block_badges_dashboard.feature create mode 100644 badges/tests/behat/block_badges_frontpage.feature create mode 100644 badges/tests/fixtures/badge.png create mode 100644 badges/version.php create mode 100644 blog_menu/block_blog_menu.php create mode 100644 blog_menu/classes/privacy/provider.php create mode 100644 blog_menu/db/access.php create mode 100644 blog_menu/lang/en/block_blog_menu.php create mode 100644 blog_menu/tests/behat/block_blog_menu.feature create mode 100644 blog_menu/tests/behat/block_blog_menu_activity.feature create mode 100644 blog_menu/tests/behat/block_blog_menu_course.feature create mode 100644 blog_menu/tests/behat/block_blog_menu_frontpage.feature create mode 100644 blog_menu/version.php create mode 100644 blog_recent/block_blog_recent.php create mode 100644 blog_recent/classes/privacy/provider.php create mode 100644 blog_recent/db/access.php create mode 100644 blog_recent/edit_form.php create mode 100644 blog_recent/lang/en/block_blog_recent.php create mode 100644 blog_recent/tests/behat/block_blog_recent.feature create mode 100644 blog_recent/tests/behat/block_blog_recent_activity.feature create mode 100644 blog_recent/tests/behat/block_blog_recent_course.feature create mode 100644 blog_recent/tests/behat/block_blog_recent_frontpage.feature create mode 100644 blog_recent/version.php create mode 100644 blog_tags/block_blog_tags.php create mode 100644 blog_tags/classes/privacy/provider.php create mode 100644 blog_tags/db/access.php create mode 100644 blog_tags/edit_form.php create mode 100644 blog_tags/lang/en/block_blog_tags.php create mode 100644 blog_tags/styles.css create mode 100644 blog_tags/tests/behat/blogtag.feature create mode 100644 blog_tags/version.php create mode 100644 calendar_month/block_calendar_month.php create mode 100644 calendar_month/classes/privacy/provider.php create mode 100644 calendar_month/db/access.php create mode 100644 calendar_month/db/upgrade.php create mode 100644 calendar_month/lang/en/block_calendar_month.php create mode 100644 calendar_month/tests/behat/block_calendar_month.feature create mode 100644 calendar_month/tests/behat/block_calendar_month_course.feature create mode 100644 calendar_month/tests/behat/block_calendar_month_dashboard.feature create mode 100644 calendar_month/tests/behat/block_calendar_month_frontpage.feature create mode 100644 calendar_month/version.php create mode 100644 calendar_upcoming/block_calendar_upcoming.php create mode 100644 calendar_upcoming/classes/privacy/provider.php create mode 100644 calendar_upcoming/db/access.php create mode 100644 calendar_upcoming/db/upgrade.php create mode 100644 calendar_upcoming/lang/en/block_calendar_upcoming.php create mode 100644 calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature create mode 100644 calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature create mode 100644 calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature create mode 100644 calendar_upcoming/upgrade.txt create mode 100644 calendar_upcoming/version.php create mode 100644 career/README.md create mode 100644 career/block_career.php create mode 100644 career/career_list.php create mode 100644 career/career_setting.php create mode 100644 career/career_unit.php create mode 100644 career/db/access.php create mode 100644 career/db/install.xml create mode 100644 career/edit_form.php create mode 100644 career/entity/block_career_ressource.php create mode 100644 career/entity/block_career_section.php create mode 100644 "career/img/Capture d\342\200\231\303\251cran 2018-06-19 \303\240 18.18.32.png" create mode 100644 career/img/file.txt create mode 100644 career/img/numoc-16-9-blanc.png create mode 100644 career/img/rose-rose.jpg create mode 100644 career/index.php create mode 100644 career/js/file.js create mode 100644 career/js/interact.min.js create mode 100644 career/js/jquery-1.8.2.min.js create mode 100644 career/js/jquery.min.js create mode 100644 career/lang/en/block_career.php create mode 100644 career/lang/fr/block_career.php create mode 100644 career/styles.css create mode 100644 career/version.php create mode 100644 career/view/view_career_list.php create mode 100644 career/view/view_career_setting.php create mode 100644 career/view/view_career_unit.php create mode 100644 classes 14.04.12/external.php create mode 100644 classes 14.04.12/privacy/provider.php create mode 100644 comments/block_comments.php create mode 100644 comments/classes/event/comment_created.php create mode 100644 comments/classes/event/comment_deleted.php create mode 100644 comments/classes/privacy/provider.php create mode 100644 comments/db/access.php create mode 100644 comments/lang/en/block_comments.php create mode 100644 comments/lib.php create mode 100644 comments/tests/behat/add_comment.feature create mode 100644 comments/tests/behat/behat_block_comments.php create mode 100644 comments/tests/behat/block_comment_activity.feature create mode 100644 comments/tests/behat/block_comment_course.feature create mode 100644 comments/tests/behat/block_comment_dashboard.feature create mode 100644 comments/tests/behat/block_comment_frontpage.feature create mode 100644 comments/tests/behat/delete_comment.feature create mode 100644 comments/tests/events_test.php create mode 100644 comments/tests/privacy_provider_test.php create mode 100644 comments/version.php create mode 100644 community/block_community.php create mode 100644 community/classes/privacy/provider.php create mode 100644 community/communitycourse.php create mode 100644 community/db/access.php create mode 100644 community/db/install.xml create mode 100644 community/db/upgrade.php create mode 100644 community/forms.php create mode 100644 community/lang/en/block_community.php create mode 100644 community/locallib.php create mode 100644 community/renderer.php create mode 100644 community/styles.css create mode 100644 community/tests/privacy_test.php create mode 100644 community/version.php create mode 100644 community/yui/comments/comments.js create mode 100644 community/yui/imagegallery/imagegallery.js create mode 100644 completionstatus/block_completionstatus.php create mode 100644 completionstatus/classes/privacy/provider.php create mode 100644 completionstatus/db/access.php create mode 100644 completionstatus/db/upgrade.php create mode 100644 completionstatus/details.php create mode 100644 completionstatus/lang/en/block_completionstatus.php create mode 100644 completionstatus/tests/behat/block_completionstatus.feature create mode 100644 completionstatus/tests/behat/block_completionstatus_activity_completion.feature create mode 100644 completionstatus/tests/behat/block_completionstatus_manual_other.feature create mode 100644 completionstatus/tests/behat/block_completionstatus_manual_self.feature create mode 100644 completionstatus/version.php create mode 100644 course_list/block_course_list.php create mode 100644 course_list/classes/privacy/provider.php create mode 100644 course_list/db/access.php create mode 100644 course_list/lang/en/block_course_list.php create mode 100644 course_list/settings.php create mode 100644 course_list/styles.css create mode 100644 course_list/tests/behat/block_course_list_category.feature create mode 100644 course_list/tests/behat/block_course_list_course.feature create mode 100644 course_list/tests/behat/block_course_list_dashboard.feature create mode 100644 course_list/tests/behat/block_course_list_frontpage.feature create mode 100644 course_list/version.php create mode 100644 course_summary/block_course_summary.php create mode 100644 course_summary/classes/privacy/provider.php create mode 100644 course_summary/db/access.php create mode 100644 course_summary/db/upgrade.php create mode 100644 course_summary/lang/en/block_course_summary.php create mode 100644 course_summary/styles.css create mode 100644 course_summary/tests/behat/block_course_summary_course.feature create mode 100644 course_summary/tests/behat/block_course_summary_frontpage.feature create mode 100644 course_summary/version.php create mode 100644 edit_form 14.04.12.php create mode 100644 feedback/block_feedback.php create mode 100644 feedback/classes/privacy/provider.php create mode 100644 feedback/db/access.php create mode 100644 feedback/db/install.php create mode 100644 feedback/lang/en/block_feedback.php create mode 100644 feedback/version.php create mode 100644 globalsearch/block_globalsearch.php create mode 100644 globalsearch/classes/privacy/provider.php create mode 100644 globalsearch/db/access.php create mode 100644 globalsearch/lang/en/block_globalsearch.php create mode 100644 globalsearch/styles.css create mode 100644 globalsearch/version.php create mode 100644 glossary_random/backup/moodle2/restore_glossary_random_block_task.class.php create mode 100644 glossary_random/block_glossary_random.php create mode 100644 glossary_random/classes/privacy/provider.php create mode 100644 glossary_random/db/access.php create mode 100644 glossary_random/edit_form.php create mode 100644 glossary_random/lang/en/block_glossary_random.php create mode 100644 glossary_random/tests/behat/glossary_random.feature create mode 100644 glossary_random/tests/behat/glossary_random_frontpage.feature create mode 100644 glossary_random/tests/behat/glossary_random_global.feature create mode 100644 glossary_random/version.php create mode 100644 html/backup/moodle1/lib.php create mode 100644 html/backup/moodle2/backup_html_block_task.class.php create mode 100644 html/backup/moodle2/restore_html_block_task.class.php create mode 100644 html/block_html.php create mode 100644 html/classes/privacy/provider.php create mode 100644 html/classes/search/content.php create mode 100644 html/db/access.php create mode 100644 html/db/upgrade.php create mode 100644 html/edit_form.php create mode 100644 html/lang/en/block_html.php create mode 100644 html/lib.php create mode 100644 html/settings.php create mode 100644 html/tests/behat/configuring_html_block.feature create mode 100644 html/tests/behat/course_block.feature create mode 100644 html/tests/behat/multiple_instances.feature create mode 100644 html/tests/privacy_provider_test.php create mode 100644 html/tests/search_content_test.php create mode 100644 html/version.php create mode 100644 index.html create mode 100644 login/block_login.php create mode 100644 login/classes/privacy/provider.php create mode 100644 login/db/access.php create mode 100644 login/lang/en/block_login.php create mode 100644 login/tests/behat/login_block.feature create mode 100644 login/version.php create mode 100644 lp/block_lp.php create mode 100644 lp/classes/output/competencies_to_review_page.php create mode 100644 lp/classes/output/plans_to_review_page.php create mode 100644 lp/classes/output/renderer.php create mode 100644 lp/classes/output/summary.php create mode 100644 lp/classes/privacy/provider.php create mode 100644 lp/competencies_to_review.php create mode 100644 lp/db/access.php create mode 100644 lp/lang/en/block_lp.php create mode 100644 lp/plans_to_review.php create mode 100644 lp/styles.css create mode 100644 lp/templates/competencies_to_review_page.mustache create mode 100644 lp/templates/plans_to_review_page.mustache create mode 100644 lp/templates/summary.mustache create mode 100644 lp/version.php create mode 160000 mahara_iena create mode 100644 mentees/block_mentees.php create mode 100644 mentees/classes/privacy/provider.php create mode 100644 mentees/db/access.php create mode 100644 mentees/edit_form.php create mode 100644 mentees/lang/en/block_mentees.php create mode 100644 mentees/tests/behat/configuring_mentees_block.feature create mode 100644 mentees/version.php create mode 100644 mnet_hosts/block_mnet_hosts.php create mode 100644 mnet_hosts/classes/privacy/provider.php create mode 100644 mnet_hosts/db/access.php create mode 100644 mnet_hosts/lang/en/block_mnet_hosts.php create mode 100644 mnet_hosts/version.php create mode 100644 moodleblock.class.php create mode 100644 myoverview/amd/build/calendar_events_repository.min.js create mode 100644 myoverview/amd/build/event_list.min.js create mode 100644 myoverview/amd/build/event_list_by_course.min.js create mode 100644 myoverview/amd/build/paging_bar.min.js create mode 100644 myoverview/amd/build/paging_content.min.js create mode 100644 myoverview/amd/build/tab_preferences.min.js create mode 100644 myoverview/amd/src/calendar_events_repository.js create mode 100644 myoverview/amd/src/event_list.js create mode 100644 myoverview/amd/src/event_list_by_course.js create mode 100644 myoverview/amd/src/paging_bar.js create mode 100644 myoverview/amd/src/paging_content.js create mode 100644 myoverview/amd/src/tab_preferences.js create mode 100644 myoverview/block_myoverview.php create mode 100644 myoverview/classes/output/courses_view.php create mode 100644 myoverview/classes/output/main.php create mode 100644 myoverview/classes/output/renderer.php create mode 100644 myoverview/classes/privacy/provider.php create mode 100644 myoverview/db/access.php create mode 100644 myoverview/lang/en/block_myoverview.php create mode 100644 myoverview/lib.php create mode 100644 myoverview/pix/activities.svg create mode 100644 myoverview/pix/courses.svg create mode 100644 myoverview/settings.php create mode 100644 myoverview/templates/course-event-list-item.mustache create mode 100644 myoverview/templates/course-event-list-items.mustache create mode 100644 myoverview/templates/course-event-list.mustache create mode 100644 myoverview/templates/course-item.mustache create mode 100644 myoverview/templates/course-paging-content-item.mustache create mode 100644 myoverview/templates/course-paging-content.mustache create mode 100644 myoverview/templates/course-summary.mustache create mode 100644 myoverview/templates/courses-view-by-status.mustache create mode 100644 myoverview/templates/courses-view-course-item.mustache create mode 100644 myoverview/templates/courses-view.mustache create mode 100644 myoverview/templates/event-list-group.mustache create mode 100644 myoverview/templates/event-list-item.mustache create mode 100644 myoverview/templates/event-list-items.mustache create mode 100644 myoverview/templates/event-list.mustache create mode 100644 myoverview/templates/main.mustache create mode 100644 myoverview/templates/paging-bar-item.mustache create mode 100644 myoverview/templates/paging-bar.mustache create mode 100644 myoverview/templates/paging-content-item.mustache create mode 100644 myoverview/templates/paging-content.mustache create mode 100644 myoverview/templates/progress-chart.mustache create mode 100644 myoverview/templates/timeline-view-courses.mustache create mode 100644 myoverview/templates/timeline-view-dates.mustache create mode 100644 myoverview/templates/timeline-view.mustache create mode 100644 myoverview/tests/behat/block_myoverview_dashboard.feature create mode 100644 myoverview/tests/behat/block_myoverview_progress.feature create mode 100644 myoverview/tests/privacy_test.php create mode 100644 myoverview/version.php create mode 100644 myprofile/block_myprofile.php create mode 100644 myprofile/classes/privacy/provider.php create mode 100644 myprofile/db/access.php create mode 100644 myprofile/edit_form.php create mode 100644 myprofile/lang/en/block_myprofile.php create mode 100644 myprofile/lang/en/deprecated.txt create mode 100644 myprofile/styles.css create mode 100644 myprofile/tests/behat/block_myprofile.feature create mode 100644 myprofile/tests/behat/block_myprofile_activity.feature create mode 100644 myprofile/tests/behat/block_myprofile_course.feature create mode 100644 myprofile/tests/behat/block_myprofile_dashboard.feature create mode 100644 myprofile/tests/behat/block_myprofile_frontpage.feature create mode 100644 myprofile/version.php create mode 100644 navigation/amd/build/ajax_response_renderer.min.js create mode 100644 navigation/amd/build/nav_loader.min.js create mode 100644 navigation/amd/build/navblock.min.js create mode 100644 navigation/amd/build/site_admin_loader.min.js create mode 100644 navigation/amd/src/ajax_response_renderer.js create mode 100644 navigation/amd/src/nav_loader.js create mode 100644 navigation/amd/src/navblock.js create mode 100644 navigation/amd/src/site_admin_loader.js create mode 100644 navigation/block_navigation.php create mode 100644 navigation/classes/privacy/provider.php create mode 100644 navigation/db/access.php create mode 100644 navigation/db/upgrade.php create mode 100644 navigation/edit_form.php create mode 100644 navigation/lang/en/block_navigation.php create mode 100644 navigation/renderer.php create mode 100644 navigation/styles.css create mode 100644 navigation/tests/behat/expand_courses_node.feature create mode 100644 navigation/tests/behat/participants_link.feature create mode 100644 navigation/tests/behat/view_my_courses.feature create mode 100644 navigation/version.php create mode 100644 news_items/block_news_items.php create mode 100644 news_items/classes/privacy/provider.php create mode 100644 news_items/db/access.php create mode 100644 news_items/lang/en/block_news_items.php create mode 100644 news_items/tests/behat/display_news.feature create mode 100644 news_items/version.php create mode 100644 online_users/block_online_users.php create mode 100644 online_users/classes/fetcher.php create mode 100644 online_users/classes/privacy/provider.php create mode 100644 online_users/db/access.php create mode 100644 online_users/lang/en/block_online_users.php create mode 100644 online_users/settings.php create mode 100644 online_users/styles.css create mode 100644 online_users/tests/behat/block_online_users_course.feature create mode 100644 online_users/tests/behat/block_online_users_dashboard.feature create mode 100644 online_users/tests/behat/block_online_users_frontpage.feature create mode 100644 online_users/tests/generator/lib.php create mode 100644 online_users/tests/generator_test.php create mode 100644 online_users/tests/online_users_test.php create mode 100644 online_users/version.php create mode 100644 participants/block_participants.php create mode 100644 participants/classes/privacy/provider.php create mode 100644 participants/db/access.php create mode 100644 participants/lang/en/block_participants.php create mode 100644 participants/tests/behat/block_participants_course.feature create mode 100644 participants/tests/behat/block_participants_frontpage.feature create mode 100644 participants/version.php create mode 100644 private_files/block_private_files.php create mode 100644 private_files/classes/privacy/provider.php create mode 100644 private_files/db/access.php create mode 100644 private_files/edit.php create mode 100644 private_files/lang/en/block_private_files.php create mode 100644 private_files/module.js create mode 100644 private_files/renderer.php create mode 100644 private_files/styles.css create mode 100644 private_files/tests/behat/block_private_files_activity.feature create mode 100644 private_files/tests/behat/block_private_files_course.feature create mode 100644 private_files/tests/behat/block_private_files_dashboard.feature create mode 100644 private_files/tests/behat/block_private_files_frontpage.feature create mode 100644 private_files/tests/fixtures/testfile.txt create mode 100644 private_files/version.php create mode 100644 quiz_results/backup/moodle2/restore_quiz_results_block_task.class.php create mode 100644 quiz_results/block_quiz_results.php create mode 100644 quiz_results/classes/privacy/provider.php create mode 100644 quiz_results/db/access.php create mode 100644 quiz_results/db/install.php create mode 100644 quiz_results/db/upgrade.php create mode 100644 quiz_results/lang/en/block_quiz_results.php create mode 100644 quiz_results/lang/en/depreciated.txt create mode 100644 quiz_results/version.php create mode 100644 recent_activity/block_recent_activity.php create mode 100644 recent_activity/classes/observer.php create mode 100644 recent_activity/classes/privacy/provider.php create mode 100644 recent_activity/db/access.php create mode 100644 recent_activity/db/events.php create mode 100644 recent_activity/db/install.xml create mode 100644 recent_activity/db/upgrade.php create mode 100644 recent_activity/lang/en/block_recent_activity.php create mode 100644 recent_activity/renderer.php create mode 100644 recent_activity/styles.css create mode 100644 recent_activity/tests/behat/structural_changes.feature create mode 100644 recent_activity/version.php create mode 100644 rss_client/backup/moodle1/lib.php create mode 100644 rss_client/backup/moodle2/backup_rss_client_block_task.class.php create mode 100644 rss_client/backup/moodle2/backup_rss_client_stepslib.php create mode 100644 rss_client/backup/moodle2/restore_rss_client_block_task.class.php create mode 100644 rss_client/backup/moodle2/restore_rss_client_stepslib.php create mode 100644 rss_client/block_rss_client.php create mode 100644 rss_client/classes/output/block.php create mode 100644 rss_client/classes/output/channel_image.php create mode 100644 rss_client/classes/output/feed.php create mode 100644 rss_client/classes/output/footer.php create mode 100644 rss_client/classes/output/item.php create mode 100644 rss_client/classes/output/renderer.php create mode 100644 rss_client/classes/privacy/provider.php create mode 100644 rss_client/db/access.php create mode 100644 rss_client/db/install.xml create mode 100644 rss_client/db/upgrade.php create mode 100644 rss_client/edit_form.php create mode 100644 rss_client/editfeed.php create mode 100644 rss_client/lang/en/block_rss_client.php create mode 100644 rss_client/managefeeds.php create mode 100644 rss_client/settings.php create mode 100644 rss_client/styles.css create mode 100644 rss_client/templates/block.mustache create mode 100644 rss_client/templates/channel_image.mustache create mode 100644 rss_client/templates/feed.mustache create mode 100644 rss_client/templates/footer.mustache create mode 100644 rss_client/templates/item.mustache create mode 100644 rss_client/tests/cron_test.php create mode 100644 rss_client/tests/privacy_test.php create mode 100644 rss_client/version.php create mode 100644 rss_client/viewfeed.php create mode 100644 search_forums/block_search_forums.php create mode 100644 search_forums/classes/output/renderer.php create mode 100644 search_forums/classes/output/search_form.php create mode 100644 search_forums/classes/privacy/provider.php create mode 100644 search_forums/db/access.php create mode 100644 search_forums/lang/en/block_search_forums.php create mode 100644 search_forums/styles.css create mode 100644 search_forums/templates/search_form.mustache create mode 100644 search_forums/tests/behat/block_search_forums_course.feature create mode 100644 search_forums/tests/behat/block_search_forums_frontpage.feature create mode 100644 search_forums/version.php create mode 100644 section_links/block_section_links.php create mode 100644 section_links/classes/privacy/provider.php create mode 100644 section_links/db/access.php create mode 100644 section_links/db/upgrade.php create mode 100644 section_links/edit_form.php create mode 100644 section_links/lang/en/block_section_links.php create mode 100644 section_links/renderer.php create mode 100644 section_links/settings.php create mode 100644 section_links/tests/behat/block_section_links_course.feature create mode 100644 section_links/version.php create mode 100644 selfcompletion/block_selfcompletion.php create mode 100644 selfcompletion/classes/privacy/provider.php create mode 100644 selfcompletion/db/access.php create mode 100644 selfcompletion/db/upgrade.php create mode 100644 selfcompletion/lang/en/block_selfcompletion.php create mode 100644 selfcompletion/version.php create mode 100644 settings/amd/build/settingsblock.min.js create mode 100644 settings/amd/src/settingsblock.js create mode 100644 settings/block_settings.php create mode 100644 settings/classes/privacy/provider.php create mode 100644 settings/db/access.php create mode 100644 settings/db/upgrade.php create mode 100644 settings/edit_form.php create mode 100644 settings/lang/en/block_settings.php create mode 100644 settings/renderer.php create mode 100644 settings/styles.css create mode 100644 settings/version.php create mode 100644 site_main_menu/block_site_main_menu.php create mode 100644 site_main_menu/classes/privacy/provider.php create mode 100644 site_main_menu/db/access.php create mode 100644 site_main_menu/lang/en/block_site_main_menu.php create mode 100644 site_main_menu/styles.css create mode 100644 site_main_menu/tests/behat/add_url.feature create mode 100644 site_main_menu/tests/behat/behat_block_site_main_menu.php create mode 100644 site_main_menu/tests/behat/edit_activities.feature create mode 100644 site_main_menu/version.php create mode 100644 social_activities/block_social_activities.php create mode 100644 social_activities/classes/privacy/provider.php create mode 100644 social_activities/db/access.php create mode 100644 social_activities/lang/en/block_social_activities.php create mode 100644 social_activities/styles.css create mode 100644 social_activities/tests/behat/behat_block_social_activities.php create mode 100644 social_activities/tests/behat/edit_activities.feature create mode 100644 social_activities/version.php create mode 100644 tag_flickr/block_tag_flickr.php create mode 100644 tag_flickr/classes/privacy/provider.php create mode 100644 tag_flickr/db/access.php create mode 100644 tag_flickr/edit_form.php create mode 100644 tag_flickr/lang/en/block_tag_flickr.php create mode 100644 tag_flickr/styles.css create mode 100644 tag_flickr/tests/behat/configuring_tag_flickr_block.feature create mode 100644 tag_flickr/version.php create mode 100644 tag_youtube/block_tag_youtube.php create mode 100644 tag_youtube/classes/privacy/provider.php create mode 100644 tag_youtube/db/access.php create mode 100644 tag_youtube/db/install.php create mode 100644 tag_youtube/edit_form.php create mode 100644 tag_youtube/lang/en/block_tag_youtube.php create mode 100644 tag_youtube/settings.php create mode 100644 tag_youtube/styles.css create mode 100644 tag_youtube/tests/block_tag_youtube_test.php create mode 100644 tag_youtube/upgrade.txt create mode 100644 tag_youtube/version.php create mode 100644 tags/backup/moodle2/restore_tags_block_task.class.php create mode 100644 tags/block_tags.php create mode 100644 tags/classes/privacy/provider.php create mode 100644 tags/db/access.php create mode 100644 tags/edit_form.php create mode 100644 tags/lang/en/block_tags.php create mode 100644 tags/tests/behat/tagcloud.feature create mode 100644 tags/version.php create mode 100644 tests/behat/add_blocks.feature create mode 100644 tests/behat/behat_blocks.php create mode 100644 tests/behat/configure_block_throughout_site.feature create mode 100644 tests/behat/hidden_block_region.feature create mode 100644 tests/behat/hide_blocks.feature create mode 100644 tests/behat/manage_blocks.feature create mode 100644 tests/behat/move_blocks.feature create mode 100644 tests/behat/restrict_available_blocks.feature create mode 100644 tests/behat/return_block_original_state.feature create mode 100644 tests/externallib_test.php create mode 100644 tests/privacy_test.php create mode 100644 upgrade 14.04.12.txt diff --git a/activity_modules/block_activity_modules.php b/activity_modules/block_activity_modules.php new file mode 100644 index 0000000..af67de2 --- /dev/null +++ b/activity_modules/block_activity_modules.php @@ -0,0 +1,106 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 Activity modules block. + * + * @package block_activity_modules + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/filelib.php'); + +class block_activity_modules extends block_list { + function init() { + $this->title = get_string('pluginname', 'block_activity_modules'); + } + + function get_content() { + global $CFG, $DB, $OUTPUT; + + if($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + $course = $this->page->course; + + require_once($CFG->dirroot.'/course/lib.php'); + + $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); + + foreach ($modfullnames as $modname => $modfullname) { + if ($modname === 'resources') { + $icon = $OUTPUT->pix_icon('icon', '', 'mod_page', array('class' => 'icon')); + $this->content->items[] = '<a href="'.$CFG->wwwroot.'/course/resources.php?id='.$course->id.'">'.$icon.$modfullname.'</a>'; + } else { + $icon = $OUTPUT->image_icon('icon', get_string('pluginname', $modname), $modname); + $this->content->items[] = '<a href="'.$CFG->wwwroot.'/mod/'.$modname.'/index.php?id='.$course->id.'">'.$icon.$modfullname.'</a>'; + } + } + + return $this->content; + } + + /** + * Returns the role that best describes this blocks contents. + * + * This returns 'navigation' as the blocks contents is a list of links to activities and resources. + * + * @return string 'navigation' + */ + public function get_aria_role() { + return 'navigation'; + } + + function applicable_formats() { + return array('all' => true, 'mod' => false, 'my' => false, 'admin' => false, + 'tag' => false); + } +} + + diff --git a/activity_modules/classes/privacy/provider.php b/activity_modules/classes/privacy/provider.php new file mode 100644 index 0000000..7f8b315 --- /dev/null +++ b/activity_modules/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_activity_modules. + * + * @package block_activity_modules + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_activity_modules\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_activity_modules implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/activity_modules/db/access.php b/activity_modules/db/access.php new file mode 100644 index 0000000..b8b982f --- /dev/null +++ b/activity_modules/db/access.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/>. + +/** + * Activity modules block caps. + * + * @package block_activity_modules + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/activity_modules: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/activity_modules/lang/en/block_activity_modules.php b/activity_modules/lang/en/block_activity_modules.php new file mode 100644 index 0000000..1bb9de5 --- /dev/null +++ b/activity_modules/lang/en/block_activity_modules.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/>. + +/** + * Strings for component 'block_activity_modules', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_activity_modules + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['activity_modules:addinstance'] = 'Add a new activities block'; +$string['pluginname'] = 'Activities'; +$string['privacy:metadata'] = 'The Activities block only shows data stored in other locations.'; diff --git a/activity_modules/tests/behat/block_activity_modules.feature b/activity_modules/tests/behat/block_activity_modules.feature new file mode 100644 index 0000000..63e6dba --- /dev/null +++ b/activity_modules/tests/behat/block_activity_modules.feature @@ -0,0 +1,159 @@ +@block @block_activity_modules +Feature: Block activity modules + In order to overview activity modules in a course + As a manager + I can add activities block in a course or on the frontpage + + Scenario: Add activities block on the frontpage + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | assign | Frontpage assignment name | Frontpage assignment description | Acceptance test site | assign0 | + | book | Frontpage book name | Frontpage book description | Acceptance test site | book0 | + | chat | Frontpage chat name | Frontpage chat description | Acceptance test site | chat0 | + | choice | Frontpage choice name | Frontpage choice description | Acceptance test site | choice0 | + | data | Frontpage database name | Frontpage database description | Acceptance test site | data0 | + | feedback | Frontpage feedback name | Frontpage feedback description | Acceptance test site | feedback0 | + | forum | Frontpage forum name | Frontpage forum description | Acceptance test site | forum0 | + | label | Frontpage label name | Frontpage label description | Acceptance test site | label0 | + | lti | Frontpage lti name | Frontpage lti description | Acceptance test site | lti0 | + | page | Frontpage page name | Frontpage page description | Acceptance test site | page0 | + | quiz | Frontpage quiz name | Frontpage quiz description | Acceptance test site | quiz0 | + | resource | Frontpage resource name | Frontpage resource description | Acceptance test site | resource0 | + | imscp | Frontpage imscp name | Frontpage imscp description | Acceptance test site | imscp0 | + | folder | Frontpage folder name | Frontpage folder description | Acceptance test site | folder0 | + | glossary | Frontpage glossary name | Frontpage glossary description | Acceptance test site | glossary0 | + | scorm | Frontpage scorm name | Frontpage scorm description | Acceptance test site | scorm0 | + | lesson | Frontpage lesson name | Frontpage lesson description | Acceptance test site | lesson0 | + | survey | Frontpage survey name | Frontpage survey description | Acceptance test site | survey0 | + | url | Frontpage url name | Frontpage url description | Acceptance test site | url0 | + | wiki | Frontpage wiki name | Frontpage wiki description | Acceptance test site | wiki0 | + | workshop | Frontpage workshop name | Frontpage workshop description | Acceptance test site | workshop0 | + + When I log in as "admin" + And I am on site homepage + And I follow "Turn editing on" + And I add the "Activities" block + And I click on "Assignments" "link" in the "Activities" "block" + Then I should see "Frontpage assignment name" + And I am on site homepage + And I click on "Chats" "link" in the "Activities" "block" + And I should see "Frontpage chat name" + And I am on site homepage + And I click on "Choices" "link" in the "Activities" "block" + And I should see "Frontpage choice name" + And I am on site homepage + And I click on "Databases" "link" in the "Activities" "block" + And I should see "Frontpage database name" + And I am on site homepage + And I click on "Feedback" "link" in the "Activities" "block" + And I should see "Frontpage feedback name" + And I am on site homepage + And I click on "Forums" "link" in the "Activities" "block" + And I should see "Frontpage forum name" + And I am on site homepage + And I click on "External tools" "link" in the "Activities" "block" + And I should see "Frontpage lti name" + And I am on site homepage + And I click on "Quizzes" "link" in the "Activities" "block" + And I should see "Frontpage quiz name" + And I am on site homepage + And I click on "Glossaries" "link" in the "Activities" "block" + And I should see "Frontpage glossary name" + And I am on site homepage + And I click on "SCORM packages" "link" in the "Activities" "block" + And I should see "Frontpage scorm name" + And I am on site homepage + And I click on "Lessons" "link" in the "Activities" "block" + And I should see "Frontpage lesson name" + And I am on site homepage + And I click on "Wikis" "link" in the "Activities" "block" + And I should see "Frontpage wiki name" + And I am on site homepage + And I click on "Workshop" "link" in the "Activities" "block" + And I should see "Frontpage workshop name" + And I am on site homepage + And I click on "Resources" "link" in the "Activities" "block" + And I should see "Frontpage book name" + And I should see "Frontpage page name" + And I should see "Frontpage resource name" + And I should see "Frontpage imscp name" + And I should see "Frontpage folder name" + And I should see "Frontpage url name" + + Scenario: Add activities block in a course + Given the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | assign | Test assignment name | Test assignment description | C1 | assign1 | + | book | Test book name | Test book description | C1 | book1 | + | chat | Test chat name | Test chat description | C1 | chat1 | + | choice | Test choice name | Test choice description | C1 | choice1 | + | data | Test database name | Test database description | C1 | data1 | + | feedback | Test feedback name | Test feedback description | C1 | feedback1 | + | folder | Test folder name | Test folder description | C1 | folder1 | + | forum | Test forum name | Test forum description | C1 | forum1 | + | glossary | Test glossary name | Test glossary description | C1 | glossary1 | + | imscp | Test imscp name | Test imscp description | C1 | imscp1 | + | label | Test label name | Test label description | C1 | label1 | + | lesson | Test lesson name | Test lesson description | C1 | lesson1 | + | lti | Test lti name | Test lti description | C1 | lti1 | + | page | Test page name | Test page description | C1 | page1 | + | quiz | Test quiz name | Test quiz description | C1 | quiz1 | + | resource | Test resource name | Test resource description | C1 | resource1 | + | scorm | Test scorm name | Test scorm description | C1 | scorm1 | + | survey | Test survey name | Test survey description | C1 | survey1 | + | url | Test url name | Test url description | C1 | url1 | + | wiki | Test wiki name | Test wiki description | C1 | wiki1 | + | workshop | Test workshop name | Test workshop description | C1 | workshop1 | + + When I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I add the "Activities" block + And I click on "Assignments" "link" in the "Activities" "block" + Then I should see "Test assignment name" + And I am on "Course 1" course homepage + And I click on "Chats" "link" in the "Activities" "block" + And I should see "Test chat name" + And I am on "Course 1" course homepage + And I click on "Choices" "link" in the "Activities" "block" + And I should see "Test choice name" + And I am on "Course 1" course homepage + And I click on "Databases" "link" in the "Activities" "block" + And I should see "Test database name" + And I am on "Course 1" course homepage + And I click on "Feedback" "link" in the "Activities" "block" + And I should see "Test feedback name" + And I am on "Course 1" course homepage + And I click on "Forums" "link" in the "Activities" "block" + And I should see "Test forum name" + And I am on "Course 1" course homepage + And I click on "External tools" "link" in the "Activities" "block" + And I should see "Test lti name" + And I am on "Course 1" course homepage + And I click on "Quizzes" "link" in the "Activities" "block" + And I should see "Test quiz name" + And I am on "Course 1" course homepage + And I click on "Glossaries" "link" in the "Activities" "block" + And I should see "Test glossary name" + And I am on "Course 1" course homepage + And I click on "SCORM packages" "link" in the "Activities" "block" + And I should see "Test scorm name" + And I am on "Course 1" course homepage + And I click on "Lessons" "link" in the "Activities" "block" + And I should see "Test lesson name" + And I am on "Course 1" course homepage + And I click on "Wikis" "link" in the "Activities" "block" + And I should see "Test wiki name" + And I am on "Course 1" course homepage + And I click on "Workshop" "link" in the "Activities" "block" + And I should see "Test workshop name" + And I am on "Course 1" course homepage + And I click on "Resources" "link" in the "Activities" "block" + And I should see "Test book name" + And I should see "Test page name" + And I should see "Test resource name" + And I should see "Test imscp name" + And I should see "Test folder name" + And I should see "Test url name" diff --git a/activity_modules/version.php b/activity_modules/version.php new file mode 100644 index 0000000..2edb45f --- /dev/null +++ b/activity_modules/version.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/>. + +/** + * Version details + * + * @package block_activity_modules + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_activity_modules'; // Full name of the plugin (used for diagnostics) diff --git a/activity_results/backup/moodle2/restore_activity_results_block_task.class.php b/activity_results/backup/moodle2/restore_activity_results_block_task.class.php new file mode 100644 index 0000000..42be292 --- /dev/null +++ b/activity_results/backup/moodle2/restore_activity_results_block_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/>. + +/** + * Define all the backup steps that will be used by the backup_block_task + * @package block_activity_results + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Specialised restore task for the activity_results block + * (using execute_after_tasks for recoding of target activity) + * + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_activity_results_block_task extends restore_block_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() { + } + + /** + * Define the associated file areas + */ + public function get_fileareas() { + return array(); // No associated fileareas. + } + + /** + * Define special handling of configdata. + */ + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata. + } + + /** + * This function, executed after all the tasks in the plan + * have been executed, will perform the recode of the + * target activity for the block. This must be done here + * and not in normal execution steps because the activity + * can be restored after the block. + */ + public function after_restore() { + global $DB; + + // Get the blockid. + $blockid = $this->get_blockid(); + + if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) { + $config = unserialize(base64_decode($configdata)); + if (!empty($config->activityparentid)) { + // Get the mapping and replace it in config. + if ($mapping = restore_dbops::get_backup_ids_record($this->get_restoreid(), + $config->activityparent, $config->activityparentid)) { + + // Update the parent module id (the id from mdl_quiz etc...) + $config->activityparentid = $mapping->newitemid; + + // Get the grade_items record to update the activitygradeitemid. + $info = $DB->get_record('grade_items', + array('iteminstance' => $config->activityparentid, 'itemmodule' => $config->activityparent)); + + // Update the activitygradeitemid the id from the grade_items table. + $config->activitygradeitemid = $info->id; + + // Encode and save the config. + $configdata = base64_encode(serialize($config)); + $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid)); + } + } + } + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + static public function define_decode_contents() { + return array(); + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + static public function define_decode_rules() { + return array(); + } +} diff --git a/activity_results/block_activity_results.php b/activity_results/block_activity_results.php new file mode 100644 index 0000000..2890415 --- /dev/null +++ b/activity_results/block_activity_results.php @@ -0,0 +1,707 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Classes to enforce the various access rules that can apply to a activity. + * + * @package block_activity_results + * @copyright 2009 Tim Hunt + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/lib/grade/constants.php'); +require_once($CFG->dirroot . '/course/lib.php'); + +define('B_ACTIVITYRESULTS_NAME_FORMAT_FULL', 1); +define('B_ACTIVITYRESULTS_NAME_FORMAT_ID', 2); +define('B_ACTIVITYRESULTS_NAME_FORMAT_ANON', 3); +define('B_ACTIVITYRESULTS_GRADE_FORMAT_PCT', 1); +define('B_ACTIVITYRESULTS_GRADE_FORMAT_FRA', 2); +define('B_ACTIVITYRESULTS_GRADE_FORMAT_ABS', 3); +define('B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE', 4); + +/** + * Block activity_results class definition. + * + * This block can be added to a course page or a activity page to display of list of + * the best/worst students/groups in a particular activity. + * + * @package block_activity_results + * @copyright 2009 Tim Hunt + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_activity_results extends block_base { + + /** + * Core function used to initialize the block. + */ + public function init() { + $this->title = get_string('pluginname', 'block_activity_results'); + } + + /** + * Allow the block to have a configuration page + * + * @return boolean + */ + public function has_config() { + return true; + } + + /** + * Core function, specifies where the block can be used. + * @return array + */ + public function applicable_formats() { + return array('course-view' => true, 'mod' => true); + } + + /** + * If this block belongs to a activity context, then return that activity's id. + * Otherwise, return 0. + * @return stdclass the activity record. + */ + public function get_owning_activity() { + global $DB; + + // Set some defaults. + $result = new stdClass(); + $result->id = 0; + + if (empty($this->instance->parentcontextid)) { + return $result; + } + $parentcontext = context::instance_by_id($this->instance->parentcontextid); + if ($parentcontext->contextlevel != CONTEXT_MODULE) { + return $result; + } + $cm = get_coursemodule_from_id($this->page->cm->modname, $parentcontext->instanceid); + if (!$cm) { + return $result; + } + // Get the grade_items id. + $rec = $DB->get_record('grade_items', array('iteminstance' => $cm->instance, 'itemmodule' => $this->page->cm->modname)); + if (!$rec) { + return $result; + } + // See if it is a gradable activity. + if (($rec->gradetype != GRADE_TYPE_VALUE) && ($rec->gradetype != GRADE_TYPE_SCALE)) { + return $result; + } + return $rec; + } + + /** + * Used to save the form config data + * @param stdclass $data + * @param bool $nolongerused + */ + public function instance_config_save($data, $nolongerused = false) { + global $DB; + if (empty($data->activitygradeitemid)) { + // Figure out info about parent module. + $info = $this->get_owning_activity(); + $data->activitygradeitemid = $info->id; + if ($info->id < 1) { + // No activity was selected. + $info->itemmodule = ''; + $info->iteminstance = ''; + } else { + $data->activityparent = $info->itemmodule; + $data->activityparentid = $info->iteminstance; + } + } else { + // Lookup info about the parent module (we have the id from mdl_grade_items. + $info = $DB->get_record('grade_items', array('id' => $data->activitygradeitemid)); + $data->activityparent = $info->itemmodule; + $data->activityparentid = $info->iteminstance; + } + parent::instance_config_save($data); + } + + /** + * Used to generate the content for the block. + * @return string + */ + public function get_content() { + global $USER, $CFG, $DB; + + if ($this->content !== null) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + // We are configured so use the configuration. + if (!empty($this->config->activitygradeitemid)) { + // We are configured. + $activitygradeitemid = $this->config->activitygradeitemid; + + // Lookup the module in the grade_items table. + $activity = $DB->get_record('grade_items', array('id' => $activitygradeitemid)); + if (empty($activity)) { + // Activity does not exist. + $this->content->text = get_string('error_emptyactivityrecord', 'block_activity_results'); + return $this->content; + } + $courseid = $activity->courseid; + $inactivity = false; + } else { + // Not configured. + $activitygradeitemid = 0; + } + + // Check to see if we are in the moule we are displaying results for. + if (!empty($this->config->activitygradeitemid)) { + if ($this->get_owning_activity()->id == $this->config->activitygradeitemid) { + $inactivity = true; + } else { + $inactivity = false; + } + } + + // Activity ID is missing. + if (empty($activitygradeitemid)) { + $this->content->text = get_string('error_emptyactivityid', 'block_activity_results'); + return $this->content; + } + + // Check to see if we are configured. + if (empty($this->config->showbest) && empty($this->config->showworst)) { + $this->content->text = get_string('configuredtoshownothing', 'block_activity_results'); + return $this->content; + } + + // Check to see if it is a supported grade type. + if (empty($activity->gradetype) || ($activity->gradetype != GRADE_TYPE_VALUE && $activity->gradetype != GRADE_TYPE_SCALE)) { + $this->content->text = get_string('error_unsupportedgradetype', 'block_activity_results'); + return $this->content; + } + + // Get the grades for this activity. + $sql = 'SELECT * FROM {grade_grades} + WHERE itemid = ? AND finalgrade is not NULL + ORDER BY finalgrade, timemodified DESC'; + + $grades = $DB->get_records_sql($sql, array( $activitygradeitemid)); + + if (empty($grades) || $activity->hidden) { + // No grades available, The block will hide itself in this case. + return $this->content; + } + + // Set up results. + $groupmode = NOGROUPS; + $best = array(); + $worst = array(); + + if (!empty($this->config->nameformat)) { + $nameformat = $this->config->nameformat; + } else { + $nameformat = B_ACTIVITYRESULTS_NAME_FORMAT_FULL; + } + + // Get $cm and context. + if ($inactivity) { + $cm = $this->page->cm; + $context = $this->page->context; + } else { + $cm = get_coursemodule_from_instance($activity->itemmodule, $activity->iteminstance, $courseid); + $context = context_module::instance($cm->id); + } + + if (!empty($this->config->usegroups)) { + $groupmode = groups_get_activity_groupmode($cm); + + if ($groupmode == SEPARATEGROUPS && has_capability('moodle/site:accessallgroups', $context)) { + // If you have the ability to see all groups then lets show them. + $groupmode = VISIBLEGROUPS; + } + } + + switch ($groupmode) { + case VISIBLEGROUPS: + // Display group-mode results. + $groups = groups_get_all_groups($courseid); + + if (empty($groups)) { + // No groups exist, sorry. + $this->content->text = get_string('error_nogroupsexist', 'block_activity_results'); + return $this->content; + } + + // Find out all the userids which have a submitted grade. + $userids = array(); + $gradeforuser = array(); + foreach ($grades as $grade) { + $userids[] = $grade->userid; + $gradeforuser[$grade->userid] = (float)$grade->finalgrade; + } + + // Now find which groups these users belong in. + list($usertest, $params) = $DB->get_in_or_equal($userids); + $params[] = $courseid; + $usergroups = $DB->get_records_sql(' + SELECT gm.id, gm.userid, gm.groupid, g.name + FROM {groups} g + LEFT JOIN {groups_members} gm ON g.id = gm.groupid + WHERE gm.userid ' . $usertest . ' AND g.courseid = ?', $params); + + // Now, iterate the grades again and sum them up for each group. + $groupgrades = array(); + foreach ($usergroups as $usergroup) { + if (!isset($groupgrades[$usergroup->groupid])) { + $groupgrades[$usergroup->groupid] = array( + 'sum' => (float)$gradeforuser[$usergroup->userid], + 'number' => 1, + 'group' => $usergroup->name); + } else { + $groupgrades[$usergroup->groupid]['sum'] += $gradeforuser[$usergroup->userid]; + $groupgrades[$usergroup->groupid]['number'] += 1; + } + } + + foreach ($groupgrades as $groupid => $groupgrade) { + $groupgrades[$groupid]['average'] = $groupgrades[$groupid]['sum'] / $groupgrades[$groupid]['number']; + } + + // Sort groupgrades according to average grade, ascending. + uasort($groupgrades, function($a, $b) { + if ($a["average"] == $b["average"]) { + return 0; + } + return ($a["average"] > $b["average"] ? 1 : -1); + }); + + // How many groups do we have with graded member submissions to show? + $numbest = empty($this->config->showbest) ? 0 : min($this->config->showbest, count($groupgrades)); + $numworst = empty($this->config->showworst) ? 0 : min($this->config->showworst, count($groupgrades) - $numbest); + + // Collect all the group results we are going to use in $best and $worst. + $remaining = $numbest; + $groupgrade = end($groupgrades); + while ($remaining--) { + $best[key($groupgrades)] = $groupgrade['average']; + $groupgrade = prev($groupgrades); + } + + $remaining = $numworst; + $groupgrade = reset($groupgrades); + while ($remaining--) { + $worst[key($groupgrades)] = $groupgrade['average']; + $groupgrade = next($groupgrades); + } + + // Ready for output! + if ($activity->gradetype == GRADE_TYPE_SCALE) { + // We must display the results using scales. + $gradeformat = B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE; + // Preload the scale. + $scale = $this->get_scale($activity->scaleid); + } else if (intval(empty($this->config->gradeformat))) { + $gradeformat = B_ACTIVITYRESULTS_GRADE_FORMAT_PCT; + } else { + $gradeformat = $this->config->gradeformat; + } + + // Generate the header. + $this->content->text .= $this->activity_link($activity, $cm); + + if ($nameformat == B_ACTIVITYRESULTS_NAME_FORMAT_FULL) { + if (has_capability('moodle/course:managegroups', $context)) { + $grouplink = $CFG->wwwroot.'/group/overview.php?id='.$courseid.'&group='; + } else if (course_can_view_participants($context)) { + $grouplink = $CFG->wwwroot.'/user/index.php?id='.$courseid.'&group='; + } else { + $grouplink = ''; + } + } + + $rank = 0; + if (!empty($best)) { + $this->content->text .= '<table class="grades"><caption>'; + if ($numbest == 1) { + $this->content->text .= get_string('bestgroupgrade', 'block_activity_results'); + } else { + $this->content->text .= get_string('bestgroupgrades', 'block_activity_results', $numbest); + } + $this->content->text .= '</caption><colgroup class="number" />'; + $this->content->text .= '<colgroup class="name" /><colgroup class="grade" /><tbody>'; + foreach ($best as $groupid => $averagegrade) { + switch ($nameformat) { + case B_ACTIVITYRESULTS_NAME_FORMAT_ANON: + case B_ACTIVITYRESULTS_NAME_FORMAT_ID: + $thisname = get_string('group'); + break; + default: + case B_ACTIVITYRESULTS_NAME_FORMAT_FULL: + if ($grouplink) { + $thisname = '<a href="'.$grouplink.$groupid.'">'.$groupgrades[$groupid]['group'].'</a>'; + } else { + $thisname = $groupgrades[$groupid]['group']; + } + break; + } + $this->content->text .= '<tr><td>'.(++$rank).'.</td><td>'.$thisname.'</td><td>'; + switch ($gradeformat) { + case B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE: + // Round answer up and locate appropriate scale. + $answer = (round($averagegrade, 0, PHP_ROUND_HALF_UP) - 1); + if (isset($scale[$answer])) { + $this->content->text .= $scale[$answer]; + } else { + // Value is not in the scale. + $this->content->text .= get_string('unknown', 'block_activity_results'); + } + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_FRA: + $this->content->text .= $this->activity_format_grade($averagegrade) + . '/' . $this->activity_format_grade($activity->grademax); + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_ABS: + $this->content->text .= $this->activity_format_grade($averagegrade); + break; + default: + case B_ACTIVITYRESULTS_GRADE_FORMAT_PCT: + $this->content->text .= $this->activity_format_grade((float)$averagegrade / + (float)$activity->grademax * 100).'%'; + break; + } + $this->content->text .= '</td></tr>'; + } + $this->content->text .= '</tbody></table>'; + } + + $rank = 0; + if (!empty($worst)) { + $worst = array_reverse($worst, true); + $this->content->text .= '<table class="grades"><caption>'; + if ($numworst == 1) { + $this->content->text .= get_string('worstgroupgrade', 'block_activity_results'); + } else { + $this->content->text .= get_string('worstgroupgrades', 'block_activity_results', $numworst); + } + $this->content->text .= '</caption><colgroup class="number" />'; + $this->content->text .= '<colgroup class="name" /><colgroup class="grade" /><tbody>'; + foreach ($worst as $groupid => $averagegrade) { + switch ($nameformat) { + case B_ACTIVITYRESULTS_NAME_FORMAT_ANON: + case B_ACTIVITYRESULTS_NAME_FORMAT_ID: + $thisname = get_string('group'); + break; + default: + case B_ACTIVITYRESULTS_NAME_FORMAT_FULL: + if ($grouplink) { + $thisname = '<a href="'.$grouplink.$groupid.'">'.$groupgrades[$groupid]['group'].'</a>'; + } else { + $thisname = $groupgrades[$groupid]['group']; + } + break; + } + $this->content->text .= '<tr><td>'.(++$rank).'.</td><td>'.$thisname.'</td><td>'; + switch ($gradeformat) { + case B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE: + // Round answer up and locate appropriate scale. + $answer = (round($averagegrade, 0, PHP_ROUND_HALF_UP) - 1); + if (isset($scale[$answer])) { + $this->content->text .= $scale[$answer]; + } else { + // Value is not in the scale. + $this->content->text .= get_string('unknown', 'block_activity_results'); + } + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_FRA: + $this->content->text .= $this->activity_format_grade($averagegrade) + . '/' . $this->activity_format_grade($activity->grademax); + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_ABS: + $this->content->text .= $this->activity_format_grade($averagegrade); + break; + default: + case B_ACTIVITYRESULTS_GRADE_FORMAT_PCT: + $this->content->text .= $this->activity_format_grade((float)$averagegrade / + (float)$activity->grademax * 100).'%'; + break; + } + $this->content->text .= '</td></tr>'; + } + $this->content->text .= '</tbody></table>'; + } + break; + + case SEPARATEGROUPS: + // This is going to be just like no-groups mode, only we 'll filter + // out the grades from people not in our group. + if (!isloggedin()) { + // Not logged in, so show nothing. + return $this->content; + } + + $mygroups = groups_get_all_groups($courseid, $USER->id); + if (empty($mygroups)) { + // Not member of a group, show nothing. + return $this->content; + } + + // Get users from the same groups as me. + list($grouptest, $params) = $DB->get_in_or_equal(array_keys($mygroups)); + $mygroupsusers = $DB->get_records_sql_menu( + 'SELECT DISTINCT userid, 1 FROM {groups_members} WHERE groupid ' . $grouptest, + $params); + + // Filter out the grades belonging to other users, and proceed as if there were no groups. + foreach ($grades as $key => $grade) { + if (!isset($mygroupsusers[$grade->userid])) { + unset($grades[$key]); + } + } + + // No break, fall through to the default case now we have filtered the $grades array. + default: + case NOGROUPS: + // Single user mode. + $numbest = empty($this->config->showbest) ? 0 : min($this->config->showbest, count($grades)); + $numworst = empty($this->config->showworst) ? 0 : min($this->config->showworst, count($grades) - $numbest); + + // Collect all the usernames we are going to need. + $remaining = $numbest; + $grade = end($grades); + while ($remaining--) { + $best[$grade->userid] = $grade->id; + $grade = prev($grades); + } + + $remaining = $numworst; + $grade = reset($grades); + while ($remaining--) { + $worst[$grade->userid] = $grade->id; + $grade = next($grades); + } + + if (empty($best) && empty($worst)) { + // Nothing to show, for some reason... + return $this->content; + } + + // Now grab all the users from the database. + $userids = array_merge(array_keys($best), array_keys($worst)); + $fields = array_merge(array('id', 'idnumber'), get_all_user_name_fields()); + $fields = implode(',', $fields); + $users = $DB->get_records_list('user', 'id', $userids, '', $fields); + + // Ready for output! + if ($activity->gradetype == GRADE_TYPE_SCALE) { + // We must display the results using scales. + $gradeformat = B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE; + // Preload the scale. + $scale = $this->get_scale($activity->scaleid); + } else if (intval(empty($this->config->gradeformat))) { + $gradeformat = B_ACTIVITYRESULTS_GRADE_FORMAT_PCT; + } else { + $gradeformat = $this->config->gradeformat; + } + + // Generate the header. + $this->content->text .= $this->activity_link($activity, $cm); + + $rank = 0; + if (!empty($best)) { + $this->content->text .= '<table class="grades"><caption>'; + if ($numbest == 1) { + $this->content->text .= get_string('bestgrade', 'block_activity_results'); + } else { + $this->content->text .= get_string('bestgrades', 'block_activity_results', $numbest); + } + $this->content->text .= '</caption><colgroup class="number" />'; + $this->content->text .= '<colgroup class="name" /><colgroup class="grade" /><tbody>'; + foreach ($best as $userid => $gradeid) { + switch ($nameformat) { + case B_ACTIVITYRESULTS_NAME_FORMAT_ID: + $thisname = get_string('user').' '.$users[$userid]->idnumber; + break; + case B_ACTIVITYRESULTS_NAME_FORMAT_ANON: + $thisname = get_string('user'); + break; + default: + case B_ACTIVITYRESULTS_NAME_FORMAT_FULL: + if (has_capability('moodle/user:viewdetails', $context)) { + $thisname = html_writer::link(new moodle_url('/user/view.php', + array('id' => $userid, 'course' => $courseid)), fullname($users[$userid])); + } else { + $thisname = fullname($users[$userid]); + } + break; + } + $this->content->text .= '<tr><td>'.(++$rank).'.</td><td>'.$thisname.'</td><td>'; + switch ($gradeformat) { + case B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE: + // Round answer up and locate appropriate scale. + $answer = (round($grades[$gradeid]->finalgrade, 0, PHP_ROUND_HALF_UP) - 1); + if (isset($scale[$answer])) { + $this->content->text .= $scale[$answer]; + } else { + // Value is not in the scale. + $this->content->text .= get_string('unknown', 'block_activity_results'); + } + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_FRA: + $this->content->text .= $this->activity_format_grade($grades[$gradeid]->finalgrade); + $this->content->text .= '/'.$this->activity_format_grade($activity->grademax); + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_ABS: + $this->content->text .= $this->activity_format_grade($grades[$gradeid]->finalgrade); + break; + default: + case B_ACTIVITYRESULTS_GRADE_FORMAT_PCT: + if ($activity->grademax) { + $this->content->text .= $this->activity_format_grade((float)$grades[$gradeid]->finalgrade / + (float)$activity->grademax * 100).'%'; + } else { + $this->content->text .= '--%'; + } + break; + } + $this->content->text .= '</td></tr>'; + } + $this->content->text .= '</tbody></table>'; + } + + $rank = 0; + if (!empty($worst)) { + $worst = array_reverse($worst, true); + $this->content->text .= '<table class="grades"><caption>'; + if ($numbest == 1) { + $this->content->text .= get_string('worstgrade', 'block_activity_results'); + } else { + $this->content->text .= get_string('worstgrades', 'block_activity_results', $numworst); + } + $this->content->text .= '</caption><colgroup class="number" />'; + $this->content->text .= '<colgroup class="name" /><colgroup class="grade" /><tbody>'; + foreach ($worst as $userid => $gradeid) { + switch ($nameformat) { + case B_ACTIVITYRESULTS_NAME_FORMAT_ID: + $thisname = get_string('user').' '.$users[$userid]->idnumber; + break; + case B_ACTIVITYRESULTS_NAME_FORMAT_ANON: + $thisname = get_string('user'); + break; + default: + case B_ACTIVITYRESULTS_NAME_FORMAT_FULL: + if (has_capability('moodle/user:viewdetails', $context)) { + $thisname = html_writer::link(new moodle_url('/user/view.php', + array('id' => $userid, 'course' => $courseid)), fullname($users[$userid])); + } else { + $thisname = fullname($users[$userid]); + } + break; + } + $this->content->text .= '<tr><td>'.(++$rank).'.</td><td>'.$thisname.'</td><td>'; + switch ($gradeformat) { + case B_ACTIVITYRESULTS_GRADE_FORMAT_SCALE: + // Round answer up and locate appropriate scale. + $answer = (round($grades[$gradeid]->finalgrade, 0, PHP_ROUND_HALF_UP) - 1); + if (isset($scale[$answer])) { + $this->content->text .= $scale[$answer]; + } else { + // Value is not in the scale. + $this->content->text .= get_string('unknown', 'block_activity_results'); + } + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_FRA: + $this->content->text .= $this->activity_format_grade($grades[$gradeid]->finalgrade); + $this->content->text .= '/'.$this->activity_format_grade($activity->grademax); + break; + case B_ACTIVITYRESULTS_GRADE_FORMAT_ABS: + $this->content->text .= $this->activity_format_grade($grades[$gradeid]->finalgrade); + break; + default: + case B_ACTIVITYRESULTS_GRADE_FORMAT_PCT: + if ($activity->grademax) { + $this->content->text .= $this->activity_format_grade((float)$grades[$gradeid]->finalgrade / + (float)$activity->grademax * 100).'%'; + } else { + $this->content->text .= '--%'; + } + break; + } + $this->content->text .= '</td></tr>'; + } + $this->content->text .= '</tbody></table>'; + } + break; + } + + return $this->content; + } + + /** + * Allows the block to be added multiple times to a single page + * @return boolean + */ + public function instance_allow_multiple() { + return true; + } + + /** + * Formats the grade to the specified decimal points + * @param float $grade + * @return string + */ + private function activity_format_grade($grade) { + if (is_null($grade)) { + return get_string('notyetgraded', 'block_activity_results'); + } + return format_float($grade, $this->config->decimalpoints); + } + + /** + * Generates the Link to the activity module when displaed outside of the module + * @param stdclass $activity + * @param stdclass $cm + * @return string + */ + private function activity_link($activity, $cm) { + + $o = html_writer::start_tag('h3'); + $o .= html_writer::link(new moodle_url('/mod/'.$activity->itemmodule.'/view.php', + array('id' => $cm->id)), format_string(($activity->itemname), true, ['context' => context_module::instance($cm->id)])); + $o .= html_writer::end_tag('h3'); + return $o; + } + + /** + * Generates a numeric array of scale entries + * @param int $scaleid + * @return array + */ + private function get_scale($scaleid) { + global $DB; + $scaletext = $DB->get_field('scale', 'scale', array('id' => $scaleid), IGNORE_MISSING); + $scale = explode ( ',', $scaletext); + return $scale; + + } +} diff --git a/activity_results/classes/privacy/provider.php b/activity_results/classes/privacy/provider.php new file mode 100644 index 0000000..d68134e --- /dev/null +++ b/activity_results/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_activity_results. + * + * @package block_activity_results + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_activity_results\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_activity_results implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/activity_results/db/access.php b/activity_results/db/access.php new file mode 100644 index 0000000..70cdede --- /dev/null +++ b/activity_results/db/access.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/>. + +/** + * Activity results block caps. + * + * @package block_activity_results + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/activity_results: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/activity_results/edit_form.php b/activity_results/edit_form.php new file mode 100644 index 0000000..0342ee6 --- /dev/null +++ b/activity_results/edit_form.php @@ -0,0 +1,127 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 form for editing Quiz results block instances. + * + * @package block_activity_results + * @copyright 2009 Tim Hunt + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/lib/grade/constants.php'); + +/** + * Form for editing activity results block instances. + * + * @copyright 2009 Tim Hunt + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_activity_results_edit_form extends block_edit_form { + /** + * The definition of the fields to use. + * + * @param MoodleQuickForm $mform + */ + protected function specific_definition($mform) { + global $DB; + + // Load defaults. + $blockconfig = get_config('block_activity_results'); + + // Fields for editing activity_results block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + // Get supported modules (Only modules using grades or scales will be listed). + $sql = 'SELECT id, itemname FROM {grade_items} WHERE courseid = ? and itemtype = ? and (gradetype = ? or gradetype = ?)'; + $params = array($this->page->course->id, 'mod', GRADE_TYPE_VALUE, GRADE_TYPE_SCALE); + $activities = $DB->get_records_sql_menu($sql, $params); + core_collator::asort($activities); + + if (empty($activities)) { + $mform->addElement('static', 'noactivitieswarning', get_string('config_select_activity', 'block_activity_results'), + get_string('config_no_activities_in_course', 'block_activity_results')); + } else { + foreach ($activities as $id => $name) { + $activities[$id] = strip_tags(format_string($name)); + } + $mform->addElement('select', 'config_activitygradeitemid', + get_string('config_select_activity', 'block_activity_results'), $activities); + $mform->setDefault('config_activitygradeitemid', $this->block->get_owning_activity()->id); + } + + $mform->addElement('text', 'config_showbest', + get_string('config_show_best', 'block_activity_results'), array('size' => 3)); + $mform->setDefault('config_showbest', $blockconfig->config_showbest); + $mform->setType('config_showbest', PARAM_INT); + if ($blockconfig->config_showbest_locked) { + $mform->freeze('config_showbest'); + } + + $mform->addElement('text', 'config_showworst', + get_string('config_show_worst', 'block_activity_results'), array('size' => 3)); + $mform->setDefault('config_showworst', $blockconfig->config_showworst); + $mform->setType('config_showworst', PARAM_INT); + if ($blockconfig->config_showworst_locked) { + $mform->freeze('config_showworst'); + } + + $mform->addElement('selectyesno', 'config_usegroups', get_string('config_use_groups', 'block_activity_results')); + $mform->setDefault('config_usegroups', $blockconfig->config_usegroups); + if ($blockconfig->config_usegroups_locked) { + $mform->freeze('config_usegroups'); + } + + $nameoptions = array( + B_ACTIVITYRESULTS_NAME_FORMAT_FULL => get_string('config_names_full', 'block_activity_results'), + B_ACTIVITYRESULTS_NAME_FORMAT_ID => get_string('config_names_id', 'block_activity_results'), + B_ACTIVITYRESULTS_NAME_FORMAT_ANON => get_string('config_names_anon', 'block_activity_results') + ); + $mform->addElement('select', 'config_nameformat', + get_string('config_name_format', 'block_activity_results'), $nameoptions); + $mform->setDefault('config_nameformat', $blockconfig->config_nameformat); + if ($blockconfig->config_nameformat_locked) { + $mform->freeze('config_nameformat'); + } + + $gradeeoptions = array( + B_ACTIVITYRESULTS_GRADE_FORMAT_PCT => get_string('config_format_percentage', 'block_activity_results'), + B_ACTIVITYRESULTS_GRADE_FORMAT_FRA => get_string('config_format_fraction', 'block_activity_results'), + B_ACTIVITYRESULTS_GRADE_FORMAT_ABS => get_string('config_format_absolute', 'block_activity_results') + ); + $mform->addElement('select', 'config_gradeformat', + get_string('config_grade_format', 'block_activity_results'), $gradeeoptions); + $mform->setDefault('config_gradeformat', $blockconfig->config_gradeformat); + if ($blockconfig->config_gradeformat_locked) { + $mform->freeze('config_gradeformat'); + } + + $options = array(); + for ($i = 0; $i <= 5; $i++) { + $options[$i] = $i; + } + $mform->addElement('select', 'config_decimalpoints', get_string('config_decimalplaces', 'block_activity_results'), + $options); + $mform->setDefault('config_decimalpoints', $blockconfig->config_decimalpoints); + $mform->setType('config_decimalpoints', PARAM_INT); + if ($blockconfig->config_decimalpoints_locked) { + $mform->freeze('config_decimalpoints'); + } + } +} \ No newline at end of file diff --git a/activity_results/lang/en/block_activity_results.php b/activity_results/lang/en/block_activity_results.php new file mode 100644 index 0000000..97dec82 --- /dev/null +++ b/activity_results/lang/en/block_activity_results.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/>. + +/** + * Strings for component 'block_activity_results', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_activity_results + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['activity_results:addinstance'] = 'Add a new activity results block'; +$string['bestgrade'] = 'The highest grade:'; +$string['bestgrades'] = 'The {$a} highest grades:'; +$string['bestgroupgrade'] = 'The group with the highest average:'; +$string['bestgroupgrades'] = 'The {$a} groups with the highest average:'; +$string['config_format_absolute'] = 'Absolute numbers'; +$string['config_format_fraction'] = 'Fractions'; +$string['config_format_percentage'] = 'Percentages'; +$string['config_decimalplaces'] = 'Decimal places to display'; +$string['config_grade_format'] = 'Display grades as'; +$string['config_name_format'] = 'Privacy of results'; +$string['config_names_anon'] = 'Anonymous results'; +$string['config_names_full'] = 'Display full names'; +$string['config_names_id'] = 'Display only ID numbers'; +$string['config_no_activities_in_course'] = 'There are not yet any activities in this course.'; +$string['config_select_activity'] = 'Which activity should this block display results from?'; +$string['config_show_best'] = 'How many of the highest grades should be shown (0 to disable)?'; +$string['config_show_worst'] = 'How many of the lowest grades should be shown (0 to disable)?'; +$string['configuredtoshownothing'] = 'This block\'s configuration currently does not allow it to show any results.'; +$string['config_use_groups'] = 'Show groups instead of students (only if the activity supports groups)?'; +$string['defaulthighestgrades'] = 'Default highest grades shown'; +$string['defaulthighestgrades_desc'] = 'How many of the highest grades should be shown by default?'; +$string['defaultlowestgrades'] = 'Default lowest grades shown'; +$string['defaultlowestgrades_desc'] = 'How many of the lowest grades should be shown by default?'; +$string['defaultshowgroups'] = 'Default show groups'; +$string['defaultnameoptions'] = 'Privacy of results'; +$string['defaultnameoptions_desc'] = 'How should the students be identified by default?'; +$string['defaultshowgroups_desc'] = 'Show groups instead of students by default (only if the activity supports groups)'; +$string['defaultgradedisplay'] = 'Display grades as'; +$string['defaultgradedisplay_desc'] = 'How should the grades be displayed by default?'; +$string['defaultdecimalplaces'] = 'Decimal places'; +$string['defaultdecimalplaces_desc'] = 'Number of decimal places to display by default'; +$string['error_emptyactivityid'] = 'Please configure this block and select which activity it should display results from.'; +$string['error_emptyactivityrecord'] = 'Error: the selected activity does not exist in the database.'; +$string['error_nogroupsexist'] = 'Error: the block is set to display grades in group mode, but there are no groups defined.'; +$string['error_unsupportedgradetype'] = 'Error: the activity selected uses a grading method that is not supported by this block.'; +$string['notyetgraded'] = 'Not yet graded'; +$string['pluginname'] = 'Activity results'; +$string['unknown'] = 'Unknown scale'; +$string['worstgrade'] = 'The lowest grade:'; +$string['worstgrades'] = 'The {$a} lowest grades:'; +$string['worstgroupgrade'] = 'The group with the lowest average:'; +$string['worstgroupgrades'] = 'The {$a} groups with the lowest average:'; +$string['privacy:metadata'] = 'The Activity results block only shows data stored in other locations.'; diff --git a/activity_results/settings.php b/activity_results/settings.php new file mode 100644 index 0000000..da08088 --- /dev/null +++ b/activity_results/settings.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 form for editing activity results block instances. + * + * @package block_activity_results + * @copyright 2016 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + + // Default high scores. + $setting = new admin_setting_configtext('block_activity_results/config_showbest', + new lang_string('defaulthighestgrades', 'block_activity_results'), + new lang_string('defaulthighestgrades_desc', 'block_activity_results'), 3, PARAM_INT); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default low scores. + $setting = new admin_setting_configtext('block_activity_results/config_showworst', + new lang_string('defaultlowestgrades', 'block_activity_results'), + new lang_string('defaultlowestgrades_desc', 'block_activity_results'), 0, PARAM_INT); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default group display. + $yesno = array(0 => get_string('no'), 1 => get_string('yes')); + $setting = new admin_setting_configselect('block_activity_results/config_usegroups', + new lang_string('defaultshowgroups', 'block_activity_results'), + new lang_string('defaultshowgroups_desc', 'block_activity_results'), 0, $yesno); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default privacy settings. + $nameoptions = array( + B_ACTIVITYRESULTS_NAME_FORMAT_FULL => get_string('config_names_full', 'block_activity_results'), + B_ACTIVITYRESULTS_NAME_FORMAT_ID => get_string('config_names_id', 'block_activity_results'), + B_ACTIVITYRESULTS_NAME_FORMAT_ANON => get_string('config_names_anon', 'block_activity_results') + ); + $setting = new admin_setting_configselect('block_activity_results/config_nameformat', + new lang_string('defaultnameoptions', 'block_activity_results'), + new lang_string('defaultnameoptions_desc', 'block_activity_results'), B_ACTIVITYRESULTS_NAME_FORMAT_FULL, $nameoptions); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default grade display settings. + $gradeoptions = array( + B_ACTIVITYRESULTS_GRADE_FORMAT_PCT => get_string('config_format_percentage', 'block_activity_results'), + B_ACTIVITYRESULTS_GRADE_FORMAT_FRA => get_string('config_format_fraction', 'block_activity_results'), + B_ACTIVITYRESULTS_GRADE_FORMAT_ABS => get_string('config_format_absolute', 'block_activity_results') + ); + $setting = new admin_setting_configselect('block_activity_results/config_gradeformat', + new lang_string('defaultgradedisplay', 'block_activity_results'), + new lang_string('defaultgradedisplay_desc', 'block_activity_results'), B_ACTIVITYRESULTS_GRADE_FORMAT_PCT, $gradeoptions); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default decimal places. + $places = array(); + for ($i = 0; $i <= 5; $i++) { + $places[$i] = $i; + } + $setting = new admin_setting_configselect('block_activity_results/config_decimalpoints', + new lang_string('defaultdecimalplaces', 'block_activity_results'), + new lang_string('defaultdecimalplaces_desc', 'block_activity_results'), 2, $places); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + +} diff --git a/activity_results/styles.css b/activity_results/styles.css new file mode 100644 index 0000000..83ffd7d --- /dev/null +++ b/activity_results/styles.css @@ -0,0 +1,28 @@ +.block_activity_results h1 { + margin: 4px; + font-size: 1.1em; +} + +.block_activity_results table.grades { + text-align: left; + width: 100%; +} + +.block_activity_results table.grades .number { + text-align: left; + width: 10%; +} + +.block_activity_results table.grades .name { + text-align: left; + width: 77%; +} + +.block_activity_results table.grades .grade { + text-align: right; +} + +.block_activity_results table.grades caption { + font-weight: bold; + font-size: 18px; +} diff --git a/activity_results/tests/behat/addblockinactivity.feature b/activity_results/tests/behat/addblockinactivity.feature new file mode 100644 index 0000000..3191e13 --- /dev/null +++ b/activity_results/tests/behat/addblockinactivity.feature @@ -0,0 +1,102 @@ +@block @block_activity_results +Feature: The activity results block displays student scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + 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 | + | student5 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment 1 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment 2 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment 3 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add a "Page" to section "1" + And I set the following fields to these values: + | Name | Test page name | + | Description | Test page description | + | Page content | This is a page | + And I press "Save and return to course" + And I am on "Course 1" course homepage + And I should see "Test page name" + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment 1" + And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment 1" + And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment 1" + And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment 1" + And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment 1" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on a non-graded activity to show 3 high scores + Given I follow "Test page name" + And I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_activitygradeitemid | Test assignment 1 | + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90.00" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "80.00" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70.00" in the "Activity results" "block" + + Scenario: Block should select current activity by default + Given I follow "Test assignment 1" + When I add the "Activity results" block + And I configure the "Activity results" block + Then the field "id_config_activitygradeitemid" matches value "Test assignment 1" + And I press "Cancel" + And I am on "Course 1" course homepage + And I follow "Test assignment 2" + And I add the "Activity results" block + And I configure the "Activity results" block + And the field "id_config_activitygradeitemid" matches value "Test assignment 2" + And I press "Cancel" + And I am on "Course 1" course homepage + And I follow "Test assignment 3" + And I add the "Activity results" block + And I configure the "Activity results" block + And the field "id_config_activitygradeitemid" matches value "Test assignment 3" + And I press "Cancel" + And I am on "Course 1" course homepage + And I follow "Test page name" + And I add the "Activity results" block + And I configure the "Activity results" block + And the field "id_config_activitygradeitemid" does not match value "Test page name" diff --git a/activity_results/tests/behat/addunconfiguredblock.feature b/activity_results/tests/behat/addunconfiguredblock.feature new file mode 100644 index 0000000..0570dbd --- /dev/null +++ b/activity_results/tests/behat/addunconfiguredblock.feature @@ -0,0 +1,42 @@ +@block @block_activity_results +Feature: The activity results block doesn't displays student scores for unconfigured block + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + + Scenario: Add the block to a the course + Given I add the "Activity results" block + Then I should see "Please configure this block and select which activity it should display results from." in the "Activity results" "block" + + Scenario: Try to configure the block on the course page in a course without activities + Given I add the "Activity results" block + When I configure the "Activity results" block + And I should see "There are not yet any activities in this course." + And I press "Save changes" + Then I should see "Please configure this block and select which activity it should display results from." in the "Activity results" "block" + + Scenario: Try to configure the block on a resource page in a course without activities + Given I add a "Page" to section "1" + And I set the following fields to these values: + | Name | Test page name | + | Description | Test page description | + | page | This is a page | + And I press "Save and display" + When I add the "Activity results" block + And I configure the "Activity results" block + And I should see "There are not yet any activities in this course." + And I press "Save changes" + Then I should see "Please configure this block and select which activity it should display results from." in the "Activity results" "block" diff --git a/activity_results/tests/behat/addunsupportedactivity.feature b/activity_results/tests/behat/addunsupportedactivity.feature new file mode 100644 index 0000000..982510d --- /dev/null +++ b/activity_results/tests/behat/addunsupportedactivity.feature @@ -0,0 +1,39 @@ +@block @block_activity_results +Feature: The activity results block doesn't display student scores for unsupported activity + In order to be display student scores + As a user + I need to properly configure the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + + Scenario: Try to configure the block to use an activity without grades + Given I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add the "Activity results" block + And I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + And I press "Save changes" + When I follow "Test assignment" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | id_grade_modgrade_type | None | + And I press "Save and return to course" + Then I should see "Error: the activity selected uses a grading method that is not supported by this block." in the "Activity results" "block" diff --git a/activity_results/tests/behat/defaultsettings.feature b/activity_results/tests/behat/defaultsettings.feature new file mode 100644 index 0000000..3af9d0e --- /dev/null +++ b/activity_results/tests/behat/defaultsettings.feature @@ -0,0 +1,64 @@ +@block @block_activity_results +Feature: The activity results block can have administrator set defaults + In order to be customize the activity results block + As an admin + I need can assign some site wide defaults + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + 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 | + + Scenario: Assign some site-wide defaults to the block. + Given the following config values are set as admin: + | config_showbest | 0 | block_activity_results | + | config_showworst | 0 | block_activity_results | + | config_gradeformat | 2 | block_activity_results | + | config_nameformat | 2 | block_activity_results | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add the "Activity results" block + When I configure the "Activity results" block + And the following fields match these values: + | id_config_showbest | 0 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display only ID numbers | + And I press "Save changes" + Then I should see "This block's configuration currently does not allow it to show any results." in the "Activity results" "block" + + Scenario: Assign some site-wide defaults to the block and lock them. + Given the following config values are set as admin: + | config_showbest | 0 | block_activity_results | + | config_showbest_locked | 1 | block_activity_results | + | config_showworst | 0 | block_activity_results | + | config_showworst_locked | 1 | block_activity_results | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I add the "Activity results" block + When I configure the "Activity results" block + And the following fields match these values: + | id_config_showbest | 0 | + | id_config_showworst | 0 | + And the "id_config_showbest" "field" should be readonly + And the "id_config_showworst" "field" should be readonly + And I press "Save changes" + Then I should see "This block's configuration currently does not allow it to show any results." in the "Activity results" "block" diff --git a/activity_results/tests/behat/highscoreswithoutgroups.feature b/activity_results/tests/behat/highscoreswithoutgroups.feature new file mode 100644 index 0000000..eb750a0 --- /dev/null +++ b/activity_results/tests/behat/highscoreswithoutgroups.feature @@ -0,0 +1,169 @@ +@block @block_activity_results +Feature: The activity results block displays student high scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + 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 | + | student5 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 0 high scores + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "This block's configuration currently does not allow it to show any results." in the "Activity results" "block" + + Scenario: Configure the block on the course page to show 1 high score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90%" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "80%" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90.00/100.00" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "80.00/100.00" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "90.00" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "80.00" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + And I press "Save changes" + Then I should see "User S1" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" + And I should see "User S2" in the "Activity results" "block" + And I should see "80.00%" in the "Activity results" "block" + And I should see "User S3" in the "Activity results" "block" + And I should see "70.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + And I press "Save changes" + Then I should see "User" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" + And I should see "80.00%" in the "Activity results" "block" + And I should see "70.00%" in the "Activity results" "block" diff --git a/activity_results/tests/behat/highscoreswithscales.feature b/activity_results/tests/behat/highscoreswithscales.feature new file mode 100644 index 0000000..2a8bcf9 --- /dev/null +++ b/activity_results/tests/behat/highscoreswithscales.feature @@ -0,0 +1,109 @@ +@block @block_activity_results +Feature: The activity results block displays students high scores in group as scales + In order to be display student scores as scales + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + 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 | + | student5 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Scales" in the course gradebook + And I press "Add a new scale" + And I set the following fields to these values: + | Name | My Scale | + | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I press "Save changes" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | id_grade_modgrade_type | Scale | + | id_grade_modgrade_scale | My Scale | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "Average" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "Not good enough" to the user "Student 5" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 high score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using full names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Display only ID numbers | + And I press "Save changes" + Then I should see "User S1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "User S2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "User S3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Anonymous results | + And I press "Save changes" + Then I should see "User" in the "Activity results" "block" + And I should not see "Student 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should not see "Student 2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should not see "Student 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" diff --git a/activity_results/tests/behat/highscoreswithscalesandgroups.feature b/activity_results/tests/behat/highscoreswithscalesandgroups.feature new file mode 100644 index 0000000..9500862 --- /dev/null +++ b/activity_results/tests/behat/highscoreswithscalesandgroups.feature @@ -0,0 +1,151 @@ +@block @block_activity_results +Feature: The activity results block displays student in group high scores as scales + In order to be display student scores as scales + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Scales" in the course gradebook + And I press "Add a new scale" + And I set the following fields to these values: + | Name | My Scale | + | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I press "Save changes" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | id_grade_modgrade_type | Scale | + | id_grade_modgrade_scale | My Scale | + | Group mode | Separate groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "Average" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Try to configure the block on the course page to show 1 high score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Student 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using full names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I should see "Student 3" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User S1" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "User S2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User" in the "Activity results" "block" + And I should see "Excellent!" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" diff --git a/activity_results/tests/behat/highscoreswithseperategroups.feature b/activity_results/tests/behat/highscoreswithseperategroups.feature new file mode 100644 index 0000000..ccc223d --- /dev/null +++ b/activity_results/tests/behat/highscoreswithseperategroups.feature @@ -0,0 +1,227 @@ +@block @block_activity_results +Feature: The activity results block displays student in separate groups scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | Group mode | Separate groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 high score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95.00/100.00" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Student 1" in the "Activity results" "block" + And I should see "100.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95.00" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Student 1" in the "Activity results" "block" + And I should see "100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95%" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85%" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Student 1" in the "Activity results" "block" + And I should see "100%" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "90%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95.00/100.00" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00/100.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I should see "Student 3" in the "Activity results" "block" + And I should see "90.00/100.00" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "80.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95.00" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Student 1" in the "Activity results" "block" + And I should see "100.00" in the "Activity results" "block" + And I should see "Student 2" in the "Activity results" "block" + And I should see "90.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "95.00%" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User S1" in the "Activity results" "block" + And I should see "100.00%" in the "Activity results" "block" + And I should see "User S2" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "95.00%" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User" in the "Activity results" "block" + And I should see "100.00%" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" diff --git a/activity_results/tests/behat/highscoreswithvisiblegroups.feature b/activity_results/tests/behat/highscoreswithvisiblegroups.feature new file mode 100644 index 0000000..11ee5a5 --- /dev/null +++ b/activity_results/tests/behat/highscoreswithvisiblegroups.feature @@ -0,0 +1,204 @@ +@block @block_activity_results +Feature: The activity results block displays student in visible groups scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | Group mode | Visible groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 high score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 1" in the "Activity results" "block" + And I should see "95%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 1" in the "Activity results" "block" + And I should see "95.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 high score as a absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 1 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 1" in the "Activity results" "block" + And I should see "95.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 1" in the "Activity results" "block" + And I should see "95%" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85%" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 1" in the "Activity results" "block" + And I should see "95.00/100.00" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00/100.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 1" in the "Activity results" "block" + And I should see "95.00" in the "Activity results" "block" + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group" in the "Activity results" "block" + And I should see "95.00%" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 3 | + | id_config_showworst | 0 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group" in the "Activity results" "block" + And I should see "95.00%" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" diff --git a/activity_results/tests/behat/lowscoreswithoutgroups.feature b/activity_results/tests/behat/lowscoreswithoutgroups.feature new file mode 100644 index 0000000..f632d95 --- /dev/null +++ b/activity_results/tests/behat/lowscoreswithoutgroups.feature @@ -0,0 +1,158 @@ +@block @block_activity_results +Feature: The activity results block displays student low scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + 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 | + | student5 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 low score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a absolute number + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50%" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "60%" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50.00/100.00" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "60.00/100.00" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "50.00" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "60.00" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "70.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + And I press "Save changes" + Then I should see "User S5" in the "Activity results" "block" + And I should see "50.00%" in the "Activity results" "block" + And I should see "User S4" in the "Activity results" "block" + And I should see "60.00%" in the "Activity results" "block" + And I should see "User S3" in the "Activity results" "block" + And I should see "70.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + And I press "Save changes" + Then I should see "User" in the "Activity results" "block" + And I should see "50.00%" in the "Activity results" "block" + And I should see "60.00%" in the "Activity results" "block" + And I should see "70.00%" in the "Activity results" "block" diff --git a/activity_results/tests/behat/lowscoreswithscales.feature b/activity_results/tests/behat/lowscoreswithscales.feature new file mode 100644 index 0000000..8885d70 --- /dev/null +++ b/activity_results/tests/behat/lowscoreswithscales.feature @@ -0,0 +1,110 @@ +@block @block_activity_results +Feature: The activity results block displays student low scores as scales + In order to be display student scores as scales + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + 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 | + | student5 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Scales" in the course gradebook + And I press "Add a new scale" + And I set the following fields to these values: + | Name | My Scale | + | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I press "Save changes" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | id_grade_modgrade_type | Scale | + | id_grade_modgrade_scale | My Scale | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "Average" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "Not good enough" to the user "Student 5" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 low score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "Not good enough" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using full names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_nameformat | Display full names | + And I press "Save changes" + Then I should see "Student 5" in the "Activity results" "block" + And I should see "Not good enough" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" + And I should see "Student 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_nameformat | Display only ID numbers | + And I press "Save changes" + Then I should see "User S5" in the "Activity results" "block" + And I should see "Not good enough" in the "Activity results" "block" + And I should see "User S4" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" + And I should see "User S3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 3 | + | id_config_nameformat | Anonymous results | + And I press "Save changes" + Then I should see "User" in the "Activity results" "block" + And I should not see "Student 5" in the "Activity results" "block" + And I should see "Not good enough" in the "Activity results" "block" + And I should not see "Student 4" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" + And I should not see "Student 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" diff --git a/activity_results/tests/behat/lowscoreswithscalesandgroups.feature b/activity_results/tests/behat/lowscoreswithscalesandgroups.feature new file mode 100644 index 0000000..a896714 --- /dev/null +++ b/activity_results/tests/behat/lowscoreswithscalesandgroups.feature @@ -0,0 +1,147 @@ +@block @block_activity_results +Feature: The activity results block displays students in groups low scores as scales + In order to be display student scores as scales + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Scales" in the course gradebook + And I press "Add a new scale" + And I set the following fields to these values: + | Name | My Scale | + | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I press "Save changes" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | id_grade_modgrade_type | Scale | + | id_grade_modgrade_scale | My Scale | + | Group mode | Separate groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "Very good" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "Good" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "Average" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Try to configure the block on the course page to show 1 low score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "Student 6" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using full names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 2" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I should see "Student 3" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "User S5" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I should see "User S6" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple high scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "Very good" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "User" in the "Activity results" "block" + And I should see "Good" in the "Activity results" "block" + And I should see "Average" in the "Activity results" "block" diff --git a/activity_results/tests/behat/lowscoreswithseperategroups.feature b/activity_results/tests/behat/lowscoreswithseperategroups.feature new file mode 100644 index 0000000..264eb8f --- /dev/null +++ b/activity_results/tests/behat/lowscoreswithseperategroups.feature @@ -0,0 +1,219 @@ +@block @block_activity_results +Feature: The activity results block displays students in separate groups scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | Group mode | Separate groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 low score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "Student 6" in the "Activity results" "block" + And I should see "70.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "Student 6" in the "Activity results" "block" + And I should see "70.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 2" in the "Activity results" "block" + And I should see "85%" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "Student 6" in the "Activity results" "block" + And I should see "70%" in the "Activity results" "block" + And I should see "Student 5" in the "Activity results" "block" + And I should see "80%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 2" in the "Activity results" "block" + And I should see "85.00/100.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I should see "Student 3" in the "Activity results" "block" + And I should see "90.00/100.00" in the "Activity results" "block" + And I should see "Student 4" in the "Activity results" "block" + And I should see "80.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 2" in the "Activity results" "block" + And I should see "85.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + And I should see "Student 5" in the "Activity results" "block" + And I should see "80.00" in the "Activity results" "block" + And I should see "Student 6" in the "Activity results" "block" + And I should see "70.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User S1" in the "Activity results" "block" + And I should see "100.00%" in the "Activity results" "block" + And I should see "User S2" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "User" in the "Activity results" "block" + And I should see "100.00%" in the "Activity results" "block" + And I should see "90.00%" in the "Activity results" "block" diff --git a/activity_results/tests/behat/lowscoreswithvisiblegroups.feature b/activity_results/tests/behat/lowscoreswithvisiblegroups.feature new file mode 100644 index 0000000..4652500 --- /dev/null +++ b/activity_results/tests/behat/lowscoreswithvisiblegroups.feature @@ -0,0 +1,200 @@ +@block @block_activity_results +Feature: The activity results block displays student in visible groups low scores + In order to be display student scores + As a user + I need to see the activity results block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + | student3 | Student | 3 | student3@example.com | S3 | + | student4 | Student | 4 | student4@example.com | S4 | + | student5 | Student | 5 | student5@example.com | S5 | + | student6 | Student | 6 | student6@example.com | S6 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + | Group 4 | C1 | G4 | + | Group 5 | C1 | G5 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + | student5 | G3 | + | student6 | G3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + | Group mode | Visible groups | + And I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" + And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" + And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" + And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" + And I press "Save changes" + And I am on "Course 1" course homepage + + Scenario: Configure the block on the course page to show 1 low score + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a fraction + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show 1 low score as a absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 1 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as percentages + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display full names | + | id_config_decimalpoints | 0 | + | id_config_usegroups | Yes | + And I press "Save changes" + Then I should see "Group 2" in the "Activity results" "block" + And I should see "85%" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + And I log out + And I log in as "student5" + And I am on "Course 1" course homepage + Then I should see "Group 2" in the "Activity results" "block" + And I should see "85%" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as fractions + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Fractions | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00/100.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00/100.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores as absolute numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Absolute numbers | + | id_config_nameformat | Display full names | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group 2" in the "Activity results" "block" + And I should see "85.00" in the "Activity results" "block" + And I should see "Group 3" in the "Activity results" "block" + And I should see "75.00" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using ID numbers + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Display only ID numbers | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" + + Scenario: Try to configure the block on the course page to show multiple low scores using anonymous names + Given I add the "Activity results" block + When I configure the "Activity results" block + And I set the following fields to these values: + | id_config_showbest | 0 | + | id_config_showworst | 2 | + | id_config_gradeformat | Percentages | + | id_config_nameformat | Anonymous results | + | id_config_usegroups | Yes | + And I press "Save changes" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Group" in the "Activity results" "block" + And I should see "85.00%" in the "Activity results" "block" + And I should see "75.00%" in the "Activity results" "block" diff --git a/activity_results/version.php b/activity_results/version.php new file mode 100644 index 0000000..6eb9031 --- /dev/null +++ b/activity_results/version.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/>. + +/** + * Version information for the block_quiz_results plugin. + * + * @package block_activity_results + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2018050800; // Requires this Moodle version. +$plugin->component = 'block_activity_results'; // Full name of the plugin (used for diagnostics). \ No newline at end of file diff --git a/admin_bookmarks/block_admin_bookmarks.php b/admin_bookmarks/block_admin_bookmarks.php new file mode 100644 index 0000000..c1dfa35 --- /dev/null +++ b/admin_bookmarks/block_admin_bookmarks.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/>. + +/** + * Admin Bookmarks Block page. + * + * @package block_admin_bookmarks + * @copyright 2011 Moodle + * @author 2006 vinkmar + * 2011 Rossiani Wijaya (updated) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The admin bookmarks block class + */ +class block_admin_bookmarks extends block_base { + + /** @var string */ + public $blockname = null; + + /** @var bool */ + protected $contentgenerated = false; + + /** @var bool|null */ + protected $docked = null; + + /** + * Set the initial properties for the block + */ + function init() { + $this->blockname = get_class($this); + $this->title = get_string('pluginname', $this->blockname); + } + + /** + * All multiple instances of this block + * @return bool Returns false + */ + function instance_allow_multiple() { + return false; + } + + /** + * Set the applicable formats for this block to all + * @return array + */ + function applicable_formats() { + if (has_capability('moodle/site:config', context_system::instance())) { + return array('all' => true); + } else { + return array('site' => true); + } + } + + /** + * Gets the content for this block + */ + function get_content() { + + global $CFG; + + // First check if we have already generated, don't waste cycles + if ($this->contentgenerated === true) { + return $this->content; + } + $this->content = new stdClass(); + + if (get_user_preferences('admin_bookmarks')) { + require_once($CFG->libdir.'/adminlib.php'); + $adminroot = admin_get_root(false, false); // settings not required - only pages + + $bookmarks = explode(',', get_user_preferences('admin_bookmarks')); + /// Accessibility: markup as a list. + $contents = array(); + foreach($bookmarks as $bookmark) { + $temp = $adminroot->locate($bookmark); + if ($temp instanceof admin_settingpage) { + $contenturl = new moodle_url('/admin/settings.php', array('section'=>$bookmark)); + $contentlink = html_writer::link($contenturl, $temp->visiblename); + $contents[] = html_writer::tag('li', $contentlink); + } else if ($temp instanceof admin_externalpage) { + $contenturl = new moodle_url($temp->url); + $contentlink = html_writer::link($contenturl, $temp->visiblename); + $contents[] = html_writer::tag('li', $contentlink); + } + } + $this->content->text = html_writer::tag('ol', implode('', $contents), array('class' => 'list')); + } else { + $bookmarks = array(); + } + + $this->content->footer = ''; + $this->page->settingsnav->initialise(); + $node = $this->page->settingsnav->get('root', navigation_node::TYPE_SITE_ADMIN); + if (!$node || !$node->contains_active_node()) { + return $this->content; + } + $section = $node->find_active_node()->key; + + if ($section == 'search' || empty($section)){ + // the search page can't be properly bookmarked at present + $this->content->footer = ''; + } else if (in_array($section, $bookmarks)) { + $deleteurl = new moodle_url('/blocks/admin_bookmarks/delete.php', array('section'=>$section, 'sesskey'=>sesskey())); + $this->content->footer = html_writer::link($deleteurl, get_string('unbookmarkthispage','admin')); + } else { + $createurl = new moodle_url('/blocks/admin_bookmarks/create.php', array('section'=>$section, 'sesskey'=>sesskey())); + $this->content->footer = html_writer::link($createurl, get_string('bookmarkthispage','admin')); + } + + return $this->content; + } + + /** + * Returns the role that best describes the admin bookmarks block. + * + * @return string + */ + public function get_aria_role() { + return 'navigation'; + } +} + + diff --git a/admin_bookmarks/classes/privacy/provider.php b/admin_bookmarks/classes/privacy/provider.php new file mode 100644 index 0000000..ac5ef87 --- /dev/null +++ b/admin_bookmarks/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_admin_bookmarks. + * + * @package block_admin_bookmarks + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_bookmarks\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_admin_bookmarks implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/admin_bookmarks/create.php b/admin_bookmarks/create.php new file mode 100644 index 0000000..ab559a3 --- /dev/null +++ b/admin_bookmarks/create.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/>. + +/** + * Create admin bookmarks. + * + * @package block_admin_bookmarks + * @copyright 2006 vinkmar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); + +require_once($CFG->libdir.'/adminlib.php'); +require_login(); +$context = context_system::instance(); +$PAGE->set_context($context); +$adminroot = admin_get_root(false, false); // settings not required - only pages + +if ($section = optional_param('section', '', PARAM_SAFEDIR) and confirm_sesskey()) { + + if (get_user_preferences('admin_bookmarks')) { + $bookmarks = explode(',', get_user_preferences('admin_bookmarks')); + + if (in_array($section, $bookmarks)) { + print_error('bookmarkalreadyexists','admin'); + die; + } + + } else { + $bookmarks = array(); + } + + $temp = $adminroot->locate($section); + + if ($temp instanceof admin_settingpage || $temp instanceof admin_externalpage) { + $bookmarks[] = $section; + $bookmarks = implode(',', $bookmarks); + set_user_preference('admin_bookmarks', $bookmarks); + + } else { + print_error('invalidsection','admin'); + die; + } + + if ($temp instanceof admin_settingpage) { + redirect($CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=' . $section); + + } elseif ($temp instanceof admin_externalpage) { + redirect($temp->url); + } + +} else { + print_error('invalidsection','admin'); + die; +} + + diff --git a/admin_bookmarks/db/access.php b/admin_bookmarks/db/access.php new file mode 100644 index 0000000..7c8b418 --- /dev/null +++ b/admin_bookmarks/db/access.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 bookmarks block caps. + * + * @package block_admin_bookmarks + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/admin_bookmarks:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/admin_bookmarks: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/admin_bookmarks/delete.php b/admin_bookmarks/delete.php new file mode 100644 index 0000000..602ef79 --- /dev/null +++ b/admin_bookmarks/delete.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/>. + +/** + * Delete admin bookmarks. + * + * @package block_admin_bookmarks + * @copyright 2006 vinkmar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); + +require_once($CFG->libdir.'/adminlib.php'); + +require_login(); +$context = context_system::instance(); +$PAGE->set_context($context); +$adminroot = admin_get_root(false, false); // settings not required - only pages + +if ($section = optional_param('section', '', PARAM_SAFEDIR) and confirm_sesskey()) { + + if (get_user_preferences('admin_bookmarks')) { + + $bookmarks = explode(',', get_user_preferences('admin_bookmarks')); + + $key = array_search($section, $bookmarks); + + if ($key === false) { + print_error('nonexistentbookmark','admin'); + die; + } + + unset($bookmarks[$key]); + $bookmarks = implode(',', $bookmarks); + set_user_preference('admin_bookmarks', $bookmarks); + + $temp = $adminroot->locate($section); + + if ($temp instanceof admin_externalpage) { + redirect($temp->url, get_string('bookmarkdeleted','admin')); + } elseif ($temp instanceof admin_settingpage) { + redirect($CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=' . $section); + } else { + redirect($CFG->wwwroot); + } + die; + + + } + + print_error('nobookmarksforuser','admin'); + die; + +} else { + print_error('invalidsection', 'admin'); + die; +} + + diff --git a/admin_bookmarks/lang/en/block_admin_bookmarks.php b/admin_bookmarks/lang/en/block_admin_bookmarks.php new file mode 100644 index 0000000..956319b --- /dev/null +++ b/admin_bookmarks/lang/en/block_admin_bookmarks.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_admin_bookmarks', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_admin_bookmarks + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['admin_bookmarks:addinstance'] = 'Add a new admin bookmarks block'; +$string['admin_bookmarks:myaddinstance'] = 'Add a new admin bookmarks block to Dashboard'; +$string['pluginname'] = 'Admin bookmarks'; +$string['privacy:metadata'] = 'The Admin bookmarks block only shows data stored in other locations.'; diff --git a/admin_bookmarks/tests/behat/bookmark_admin_pages.feature b/admin_bookmarks/tests/behat/bookmark_admin_pages.feature new file mode 100644 index 0000000..1574fe8 --- /dev/null +++ b/admin_bookmarks/tests/behat/bookmark_admin_pages.feature @@ -0,0 +1,36 @@ +@block @block_admin_bookmarks +Feature: Add a bookmarks to an admin pages + In order to speed up common tasks + As an admin + I need to add and access pages through bookmarks + + Background: + Given I log in as "admin" + And I navigate to "Scheduled tasks" node in "Site administration > Server" + And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block" + And I log out + + # Test bookmark functionality using the "User profile fields" page as our bookmark. + Scenario: Admin page can be bookmarked + Given I log in as "admin" + And I navigate to "User profile fields" node in "Site administration > Users > Accounts" + When I click on "Bookmark this page" "link" in the "Admin bookmarks" "block" + Then I should see "User profile fields" in the "Admin bookmarks" "block" + # See the existing bookmark is there too. + And I should see "Scheduled tasks" in the "Admin bookmarks" "block" + + Scenario: Admin page can be accessed through bookmarks block + Given I log in as "admin" + And I navigate to "Notifications" node in "Site administration" + And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block" + # Verify that we are on the right page. + Then I should see "Scheduled tasks" in the "h1" "css_element" + + Scenario: Admin page can be removed from bookmarks + Given I log in as "admin" + And I navigate to "Notifications" node in "Site administration" + And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block" + When I click on "Unbookmark this page" "link" in the "Admin bookmarks" "block" + Then I should see "Bookmark deleted" + And I wait to be redirected + And I should not see "Scheduled tasks" in the "Admin bookmarks" "block" diff --git a/admin_bookmarks/version.php b/admin_bookmarks/version.php new file mode 100644 index 0000000..ee047c1 --- /dev/null +++ b/admin_bookmarks/version.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/>. + +/** + * Version details + * + * @package block_admin_bookmarks + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_admin_bookmarks'; // Full name of the plugin (used for diagnostics) diff --git a/badges/block_badges.php b/badges/block_badges.php new file mode 100644 index 0000000..8fb51b6 --- /dev/null +++ b/badges/block_badges.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/>. + +/** + * Block for displaying earned local badges to users + * + * @package block_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/badgeslib.php"); + +/** + * Displays recent badges + */ +class block_badges extends block_base { + + public function init() { + $this->title = get_string('pluginname', 'block_badges'); + } + + public function instance_allow_multiple() { + return true; + } + + public function has_config() { + return false; + } + + public function instance_allow_config() { + return true; + } + + public function applicable_formats() { + return array( + 'admin' => false, + 'site-index' => true, + 'course-view' => true, + 'mod' => false, + 'my' => true + ); + } + + public function specialization() { + if (empty($this->config->title)) { + $this->title = get_string('pluginname', 'block_badges'); + } else { + $this->title = $this->config->title; + } + } + + public function get_content() { + global $USER, $PAGE, $CFG; + + if ($this->content !== null) { + return $this->content; + } + + if (empty($this->config)) { + $this->config = new stdClass(); + } + + // Number of badges to display. + if (!isset($this->config->numberofbadges)) { + $this->config->numberofbadges = 10; + } + + // Create empty content. + $this->content = new stdClass(); + $this->content->text = ''; + + if (empty($CFG->enablebadges)) { + $this->content->text .= get_string('badgesdisabled', 'badges'); + return $this->content; + } + + $courseid = $this->page->course->id; + if ($courseid == SITEID) { + $courseid = null; + } + + if ($badges = badges_get_user_badges($USER->id, $courseid, 0, $this->config->numberofbadges)) { + $output = $this->page->get_renderer('core', 'badges'); + $this->content->text = $output->print_badges_list($badges, $USER->id, true); + } else { + $this->content->text .= get_string('nothingtodisplay', 'block_badges'); + } + + return $this->content; + } +} \ No newline at end of file diff --git a/badges/classes/privacy/provider.php b/badges/classes/privacy/provider.php new file mode 100644 index 0000000..bf9721c --- /dev/null +++ b/badges/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_badges. + * + * @package block_badges + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_badges\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_badges implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/badges/db/access.php b/badges/db/access.php new file mode 100644 index 0000000..eee5e88 --- /dev/null +++ b/badges/db/access.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/>. + +/** + * Latest badges block capabilities. + * + * @package block_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> + */ + +$capabilities = array( + 'block/badges:addinstance' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + 'block/badges:myaddinstance' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW, + ), + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), +); \ No newline at end of file diff --git a/badges/db/upgrade.php b/badges/db/upgrade.php new file mode 100644 index 0000000..bb9b3cf --- /dev/null +++ b/badges/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/>. + +/** + * This file keeps track of upgrades to the badges block + * + * 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. + * + * @since Moodle 2.8 + * @package block_badges + * @copyright 2014 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade the badges block + * @param int $oldversion + * @param object $block + */ +function xmldb_block_badges_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/badges/edit_form.php b/badges/edit_form.php new file mode 100644 index 0000000..eb0767d --- /dev/null +++ b/badges/edit_form.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/>. + +/** + * Form for editing badges block instances. + * + * @package block_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> + */ + +class block_badges_edit_form extends block_edit_form { + protected function specific_definition($mform) { + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $numberofbadges = array('0' => get_string('all')); + for ($i = 1; $i <= 20; $i++) { + $numberofbadges[$i] = $i; + } + + $mform->addElement('select', 'config_numberofbadges', get_string('numbadgestodisplay', 'block_badges'), $numberofbadges); + $mform->setDefault('config_numberofbadges', 10); + } +} \ No newline at end of file diff --git a/badges/lang/en/block_badges.php b/badges/lang/en/block_badges.php new file mode 100644 index 0000000..d1cb144 --- /dev/null +++ b/badges/lang/en/block_badges.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/>. + +/** + * Language file for block "badges" + * + * @package block_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> + */ + +$string['pluginname'] = 'Latest badges'; +$string['numbadgestodisplay'] = 'Number of latest badges to display'; +$string['nothingtodisplay'] = 'You have no badges to display'; +$string['badges:addinstance'] = 'Add a new My latest badges block'; +$string['badges:myaddinstance'] = 'Add a new My latest badges block to Dashboard'; +$string['privacy:metadata'] = 'The Latest badges block only shows data stored in other locations.'; diff --git a/badges/tests/behat/block_badges.feature b/badges/tests/behat/block_badges.feature new file mode 100644 index 0000000..6586d5f --- /dev/null +++ b/badges/tests/behat/block_badges.feature @@ -0,0 +1,32 @@ +@block @block_badges +Feature: Enable Block Badges in a course without badges + In order to view the badges block in a course + As a teacher + I can add badges block to a course and view the contents + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Add the block to a the course when badges are disabled + Given I log in as "admin" + And the following config values are set as admin: + | enablebadges | 0 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Latest badges" block + Then I should see "Badges are not enabled on this site." in the "Latest badges" "block" + + Scenario: Add the block to a the course when badges are enabled + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Latest badges" block + Then I should see "You have no badges to display" in the "Latest badges" "block" diff --git a/badges/tests/behat/block_badges_course.feature b/badges/tests/behat/block_badges_course.feature new file mode 100644 index 0000000..33fe576 --- /dev/null +++ b/badges/tests/behat/block_badges_course.feature @@ -0,0 +1,71 @@ +@block @block_badges @core_badges @_file_upload @javascript +Feature: Enable Block Badges in a course + In order to enable the badges block in a course + As a teacher + I can add badges block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage + # Issue badge 1 of 2 + And I navigate to "Add a new badge" node in "Course administration > Badges" + And I set the following fields to these values: + | id_name | Badge 1 | + | id_description | Badge 1 | + | id_issuername | Teacher 1 | + And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager + And I press "Create badge" + And I select "Manual issue by role" from the "Add badge criteria" singleselect + And I set the field "Teacher" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Recipients (0)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" + And I press "Award badge" + # Issue Badge 2 of 2 + And I navigate to "Add a new badge" node in "Course administration > Badges" + And I set the following fields to these values: + | id_name | Badge 2 | + | id_description | Badge 2 | + | id_issuername | Teacher 1 | + And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager + And I press "Create badge" + And I select "Manual issue by role" from the "Add badge criteria" singleselect + And I set the field "Teacher" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Recipients (0)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" + And I press "Award badge" + And I log out + + Scenario: Add the recent badges block to a course. + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Latest badges" block + Then I should see "Badge 1" in the "Latest badges" "block" + And I should see "Badge 2" in the "Latest badges" "block" + + Scenario: Add the recent badges block to a course and limit it to only display 1 badge. + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Latest badges" block + And I configure the "Latest badges" block + And I set the following fields to these values: + | id_config_numberofbadges | 1 | + And I press "Save changes" + Then I should see "Badge 2" in the "Latest badges" "block" + And I should not see "Badge 1" in the "Latest badges" "block" diff --git a/badges/tests/behat/block_badges_dashboard.feature b/badges/tests/behat/block_badges_dashboard.feature new file mode 100644 index 0000000..2928bf3 --- /dev/null +++ b/badges/tests/behat/block_badges_dashboard.feature @@ -0,0 +1,38 @@ +@block @block_badges @core_badges @_file_upload @javascript +Feature: Enable Block Badges on the dashboard and view awarded badges + In order to view recent badges on the dashboard + As a teacher + I can add badges block to the dashboard + + Scenario: Add the recent badges block to a course. + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage + # Issue badge 1 of 2 + And I navigate to "Add a new badge" node in "Course administration > Badges" + And I set the following fields to these values: + | id_name | Badge 1 | + | id_description | Badge 1 | + | id_issuername | Teacher 1 | + And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager + And I press "Create badge" + And I select "Manual issue by role" from the "Add badge criteria" singleselect + And I set the field "Teacher" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Recipients (0)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" + And I press "Award badge" + And I log out + When I log in as "teacher1" + Then I should see "Badge 1" in the "Latest badges" "block" diff --git a/badges/tests/behat/block_badges_frontpage.feature b/badges/tests/behat/block_badges_frontpage.feature new file mode 100644 index 0000000..d1daf80 --- /dev/null +++ b/badges/tests/behat/block_badges_frontpage.feature @@ -0,0 +1,44 @@ +@block @block_badges @core_badges @_file_upload @javascript +Feature: Enable Block Badges on the frontpage and view awarded badges + In order to enable the badges block on the frontpage + As a admin + I can add badges block to the frontpage + + Scenario: Add the recent badges block on the frontpage and view recent badges + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Latest badges" block + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + # Issue badge 1 of 2 + And I navigate to "Add a new badge" node in "Course administration > Badges" + And I set the following fields to these values: + | id_name | Badge 1 | + | id_description | Badge 1 | + | id_issuername | Teacher 1 | + And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager + And I press "Create badge" + And I select "Manual issue by role" from the "Add badge criteria" singleselect + And I set the field "Teacher" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Recipients (0)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" + And I press "Award badge" + And I log out + When I log in as "teacher1" + And I am on site homepage + Then I should see "Badge 1" in the "Latest badges" "block" diff --git a/badges/tests/fixtures/badge.png b/badges/tests/fixtures/badge.png new file mode 100644 index 0000000000000000000000000000000000000000..73f2c07e52a24ed4d342635bf82c69d3ab047d79 GIT binary patch literal 2116 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 literal 0 HcmV?d00001 diff --git a/badges/version.php b/badges/version.php new file mode 100644 index 0000000..ff4cda3 --- /dev/null +++ b/badges/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2018050800; // Requires this Moodle version. +$plugin->component = 'block_badges'; diff --git a/blog_menu/block_blog_menu.php b/blog_menu/block_blog_menu.php new file mode 100644 index 0000000..71fd422 --- /dev/null +++ b/blog_menu/block_blog_menu.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/>. + +/** + * Blog Menu Block page. + * + * @package block_blog_menu + * @copyright 2009 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * The blog menu block class + */ +class block_blog_menu extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_blog_menu'); + } + + function instance_allow_multiple() { + return true; + } + + function has_config() { + return false; + } + + function applicable_formats() { + return array('all' => true, 'my' => false, 'tag' => false); + } + + function instance_allow_config() { + return true; + } + + function get_content() { + global $CFG; + + // detect if blog enabled + if ($this->content !== NULL) { + return $this->content; + } + + if (empty($CFG->enableblogs)) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('blogdisable', 'blog'); + } + return $this->content; + + } else if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL and (!isloggedin() or isguestuser())) { + $this->content = new stdClass(); + $this->content->text = ''; + return $this->content; + } + + // require necessary libs and get content + require_once($CFG->dirroot .'/blog/lib.php'); + + // Prep the content + $this->content = new stdClass(); + + $options = blog_get_all_options($this->page); + if (count($options) == 0) { + $this->content->text = ''; + return $this->content; + } + + // Iterate the option types + $menulist = array(); + foreach ($options as $types) { + foreach ($types as $link) { + $menulist[] = html_writer::link($link['link'], $link['string']); + } + $menulist[] = '<hr />'; + } + // Remove the last element (will be an HR) + array_pop($menulist); + // Display the content as a list + $this->content->text = html_writer::alist($menulist, array('class'=>'list')); + + // Prepare the footer for this block + if (has_capability('moodle/blog:search', context_system::instance())) { + // Full-text search field + $form = html_writer::tag('label', get_string('search', 'admin'), array('for'=>'blogsearchquery', 'class'=>'accesshide')); + $form .= html_writer::empty_tag('input', array('id'=>'blogsearchquery', 'type'=>'text', 'name'=>'search')); + $form .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('search'))); + $this->content->footer = html_writer::tag('form', html_writer::tag('div', $form), array('class'=>'blogsearchform', 'method'=>'get', 'action'=>new moodle_url('/blog/index.php'))); + } else { + // No footer to display + $this->content->footer = ''; + } + + // Return the content object + return $this->content; + } + + /** + * Returns the role that best describes the blog menu block. + * + * @return string + */ + public function get_aria_role() { + return 'navigation'; + } +} diff --git a/blog_menu/classes/privacy/provider.php b/blog_menu/classes/privacy/provider.php new file mode 100644 index 0000000..8850872 --- /dev/null +++ b/blog_menu/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_blog_menu. + * + * @package block_blog_menu + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_blog_menu\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_blog_menu implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/blog_menu/db/access.php b/blog_menu/db/access.php new file mode 100644 index 0000000..8cbf9be --- /dev/null +++ b/blog_menu/db/access.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/>. + +/** + * Blog menu block caps. + * + * @package block_blog_menu + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/blog_menu: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/blog_menu/lang/en/block_blog_menu.php b/blog_menu/lang/en/block_blog_menu.php new file mode 100644 index 0000000..da6f57d --- /dev/null +++ b/blog_menu/lang/en/block_blog_menu.php @@ -0,0 +1,28 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_blog_menu', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_blog_menu + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['blog_menu:addinstance'] = 'Add a new blog menu block'; +$string['pluginname'] = 'Blog menu'; +$string['privacy:metadata'] = 'The Blog menu block only shows data stored in other locations.'; diff --git a/blog_menu/tests/behat/block_blog_menu.feature b/blog_menu/tests/behat/block_blog_menu.feature new file mode 100644 index 0000000..419406b --- /dev/null +++ b/blog_menu/tests/behat/block_blog_menu.feature @@ -0,0 +1,76 @@ +@block @block_blog_menu +Feature: Enable Block blog menu in a course + In order to enable the blog menu in a course + As a teacher + I can add blog menu block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Add the block to a the course when blogs are disabled + Given I log in as "admin" + And the following config values are set as admin: + | enableblogs | 0 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should see "Blogging is disabled!" in the "Blog menu" "block" + + Scenario: Add the block to a the course when blog associations are disabled + Given I log in as "admin" + And the following config values are set as admin: + | useblogassociations | 0 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should see "Blog entries" in the "Blog menu" "block" + And I should see "Add a new entry" in the "Blog menu" "block" + And I should not see "View all entries for this course" in the "Blog menu" "block" + And I should not see "View my entries about this course" in the "Blog menu" "block" + And I should not see "Add an entry about this course" in the "Blog menu" "block" + + Scenario: Add the block to a the course when blog associations are enabled + Given I log in as "admin" + And the following config values are set as admin: + | useblogassociations | 1 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should see "Blog entries" in the "Blog menu" "block" + And I should see "Add a new entry" in the "Blog menu" "block" + And I should see "View all entries for this course" in the "Blog menu" "block" + And I should see "View my entries about this course" in the "Blog menu" "block" + And I should see "Add an entry about this course" in the "Blog menu" "block" + + Scenario: Add the block to a the course when RSS is disabled + Given I log in as "admin" + And the following config values are set as admin: + | enablerssfeeds | 0 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should not see "Blog RSS feed" in the "Blog menu" "block" + And I should see "Add a new entry" in the "Blog menu" "block" + + Scenario: Add the block to a the course when RSS is enabled + Given I log in as "admin" + And the following config values are set as admin: + | enablerssfeeds | 1 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should see "Blog RSS feed" in the "Blog menu" "block" + And I should see "Add a new entry" in the "Blog menu" "block" diff --git a/blog_menu/tests/behat/block_blog_menu_activity.feature b/blog_menu/tests/behat/block_blog_menu_activity.feature new file mode 100644 index 0000000..9a342d2 --- /dev/null +++ b/blog_menu/tests/behat/block_blog_menu_activity.feature @@ -0,0 +1,213 @@ +@block @block_blog_menu +Feature: Enable Block blog menu in an activity + In order to enable the blog menu in an activity + As a teacher + I can add blog menu block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + 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 I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment 1 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I follow "Test assignment 1" + And I add the "Blog menu" block + And I log out + + Scenario: Students use the blog menu block to post blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add a new entry" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Blog entries" + And I should see "S1 First Blog" + And I should see "This is my awesome blog!" + + Scenario: Students use the blog menu block to view their blogs about the activity + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this Assignment! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this Assignment!" + And I should see "Associated Assignment: Test assignment 1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + When I follow "View my entries about this Assignment" + Then I should see "S2 First Blog" + And I should not see "S2 Second Blog" + And I should not see "S1 First Blog" + + Scenario: Students use the blog menu block to view all blogs about the assignment + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this Assignment! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this Assignment!" + And I should see "Associated Assignment: Test assignment 1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + When I follow "View all entries about this Assignment" + Then I should see "S1 First Blog" + And I should see "S2 First Blog" + And I should not see "S2 Second Blog" + + Scenario: Students use the blog menu block to view all their blog entries + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this Assignment! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this Assignment!" + And I should see "Associated Assignment: Test assignment 1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + When I follow "Blog entries" + Then I should see "S2 First Blog" + And I should see "S2 Second Blog" + And I should not see "S1 First Blog" + + Scenario: Teacher searches for student blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this Assignment! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this Assignment!" + And I should see "Associated Assignment: Test assignment 1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Assignment: Test assignment 1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Assignment: Test assignment 1" + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I set the field "blogsearchquery" to "First" + And I press "Search" + Then I should see "S1 First Blog" + And I should see "S2 First Blog" + And I should not see "S2 Second Blog" diff --git a/blog_menu/tests/behat/block_blog_menu_course.feature b/blog_menu/tests/behat/block_blog_menu_course.feature new file mode 100644 index 0000000..39821d2 --- /dev/null +++ b/blog_menu/tests/behat/block_blog_menu_course.feature @@ -0,0 +1,190 @@ +@block @block_blog_menu +Feature: Students can use block blog menu in a course + In order students to use the blog menu in a course + As a student + I view blog menu block in a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + 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 I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Blog menu" block + And I log out + + Scenario: Students use the blog menu block to post blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add a new entry" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I follow "Blog entries" + And I should see "S1 First Blog" + And I should see "This is my awesome blog!" + + Scenario: Students use the blog menu block to view their blogs about the course + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this course! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this course!" + And I should see "Associated Course: C1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Course: C1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Course: C1" + And I am on "Course 1" course homepage + When I follow "View my entries about this course" + Then I should see "S2 First Blog" + And I should not see "S2 Second Blog" + And I should not see "S1 First Blog" + + Scenario: Students use the blog menu block to view all blogs about the course + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this course! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this course!" + And I should see "Associated Course: C1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Course: C1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Course: C1" + And I am on "Course 1" course homepage + When I follow "View all entries for this course" + Then I should see "S1 First Blog" + And I should see "S2 First Blog" + And I should not see "S2 Second Blog" + + Scenario: Students use the blog menu block to view all their blog entries + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this course! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this course!" + And I should see "Associated Course: C1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Course: C1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Course: C1" + And I am on "Course 1" course homepage + When I follow "Blog entries" + Then I should see "S2 First Blog" + And I should see "S2 Second Blog" + And I should not see "S1 First Blog" + + Scenario: Teacher searches for student blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog about this course! | + And I press "Save changes" + And I should see "S1 First Blog" + And I should see "This is my awesome blog about this course!" + And I should see "Associated Course: C1" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Add a new entry" + And I set the following fields to these values: + | Entry title | S2 Second Blog | + | Blog entry body | My unrelated blog! | + And I press "Save changes" + And I should see "S2 Second Blog" + And I should see "My unrelated blog!" + And I should not see "Associated Course: C1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + And I set the following fields to these values: + | Entry title | S2 First Blog | + | Blog entry body | My course blog is better! | + And I press "Save changes" + And I should see "S2 First Blog" + And I should see "My course blog is better!" + And I should see "Associated Course: C1" + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I set the field "blogsearchquery" to "First" + And I press "Search" + Then I should see "S1 First Blog" + And I should see "S2 First Blog" + And I should not see "S2 Second Blog" diff --git a/blog_menu/tests/behat/block_blog_menu_frontpage.feature b/blog_menu/tests/behat/block_blog_menu_frontpage.feature new file mode 100644 index 0000000..3c2936a --- /dev/null +++ b/blog_menu/tests/behat/block_blog_menu_frontpage.feature @@ -0,0 +1,30 @@ +@block @block_blog_menu +Feature: Enable Block blog menu on the frontpage + In order to enable the blog menu on the frontpage + As an admin + I can add blog menu block to the frontpage + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Blog menu" block + And I log out + + Scenario: Students use the blog menu block to post blogs + Given I log in as "student1" + And I am on site homepage + And I follow "Add a new entry" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I am on site homepage + And I follow "Blog entries" + And I should see "S1 First Blog" + And I should see "This is my awesome blog!" diff --git a/blog_menu/version.php b/blog_menu/version.php new file mode 100644 index 0000000..0d96478 --- /dev/null +++ b/blog_menu/version.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/>. + +/** + * Version details + * + * @package block_blog_menu + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_blog_menu'; // Full name of the plugin (used for diagnostics) diff --git a/blog_recent/block_blog_recent.php b/blog_recent/block_blog_recent.php new file mode 100644 index 0000000..80befeb --- /dev/null +++ b/blog_recent/block_blog_recent.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/>. + +/** + * Recent Blog Entries Block page. + * + * @package block_blog_recent + * @copyright 2009 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This block simply outputs a list of links to recent blog entries, depending on + * the context of the current page. + */ +class block_blog_recent extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_blog_recent'); + $this->content_type = BLOCK_TYPE_TEXT; + } + + function applicable_formats() { + return array('all' => true, 'my' => false, 'tag' => false); + } + + function instance_allow_config() { + return true; + } + + function get_content() { + global $CFG; + + if ($this->content !== NULL) { + return $this->content; + } + + // verify blog is enabled + if (empty($CFG->enableblogs)) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('blogdisable', 'blog'); + } + return $this->content; + + } else if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL and (!isloggedin() or isguestuser())) { + $this->content = new stdClass(); + $this->content->text = ''; + return $this->content; + } + + require_once($CFG->dirroot .'/blog/lib.php'); + require_once($CFG->dirroot .'/blog/locallib.php'); + + if (empty($this->config)) { + $this->config = new stdClass(); + } + + if (empty($this->config->recentbloginterval)) { + $this->config->recentbloginterval = 8400; + } + + if (empty($this->config->numberofrecentblogentries)) { + $this->config->numberofrecentblogentries = 4; + } + + $this->content = new stdClass(); + $this->content->footer = ''; + $this->content->text = ''; + + $context = $this->page->context; + + $url = new moodle_url('/blog/index.php'); + $filter = array(); + if ($context->contextlevel == CONTEXT_MODULE) { + $filter['module'] = $context->instanceid; + $a = new stdClass; + $a->type = get_string('modulename', $this->page->cm->modname); + $strview = get_string('viewallmodentries', 'blog', $a); + $url->param('modid', $context->instanceid); + } else if ($context->contextlevel == CONTEXT_COURSE) { + $filter['course'] = $context->instanceid; + $a = new stdClass; + $a->type = get_string('course'); + $strview = get_string('viewblogentries', 'blog', $a); + $url->param('courseid', $context->instanceid); + } else { + $strview = get_string('viewsiteentries', 'blog'); + } + $filter['since'] = $this->config->recentbloginterval; + + $bloglisting = new blog_listing($filter); + $entries = $bloglisting->get_entries(0, $this->config->numberofrecentblogentries, 4); + + if (!empty($entries)) { + $entrieslist = array(); + $viewblogurl = new moodle_url('/blog/index.php'); + + foreach ($entries as $entryid => $entry) { + $viewblogurl->param('entryid', $entryid); + $entrylink = html_writer::link($viewblogurl, shorten_text($entry->subject)); + $entrieslist[] = $entrylink; + } + + $this->content->text .= html_writer::alist($entrieslist, array('class'=>'list')); + $viewallentrieslink = html_writer::link($url, $strview); + $this->content->text .= $viewallentrieslink; + } else { + $this->content->text .= get_string('norecentblogentries', 'block_blog_recent'); + } + } +} diff --git a/blog_recent/classes/privacy/provider.php b/blog_recent/classes/privacy/provider.php new file mode 100644 index 0000000..2b33898 --- /dev/null +++ b/blog_recent/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_blog_recent. + * + * @package block_blog_recent + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_blog_recent\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_blog_recent implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/blog_recent/db/access.php b/blog_recent/db/access.php new file mode 100644 index 0000000..c501ac7 --- /dev/null +++ b/blog_recent/db/access.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/>. + +/** + * Blog recent block caps. + * + * @package block_blog_recent + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/blog_recent: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/blog_recent/edit_form.php b/blog_recent/edit_form.php new file mode 100644 index 0000000..6583ba8 --- /dev/null +++ b/blog_recent/edit_form.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/>. + +/** + * Form for editing tag block instances. + * + * @package block_blog_recent + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing tag block instances. + * + * @package block_blog_recent + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_blog_recent_edit_form extends block_edit_form { + protected function specific_definition($mform) { + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $numberofentries = array(); + for ($i = 1; $i <= 20; $i++) { + $numberofentries[$i] = $i; + } + + $mform->addElement('select', 'config_numberofrecentblogentries', get_string('numentriestodisplay', 'block_blog_recent'), $numberofentries); + $mform->setDefault('config_numberofrecentblogentries', 4); + + + $intervals = array( + 7200 => get_string('numhours', '', 2), + 14400 => get_string('numhours', '', 4), + 21600 => get_string('numhours', '', 6), + 43200 => get_string('numhours', '', 12), + 86400 => get_string('numhours', '', 24), + 172800 => get_string('numdays', '', 2), + 604800 => get_string('numdays', '', 7) + ); + + $mform->addElement('select', 'config_recentbloginterval', get_string('recentinterval', 'block_blog_recent'), $intervals); + $mform->setDefault('config_recentbloginterval', 86400); + } +} diff --git a/blog_recent/lang/en/block_blog_recent.php b/blog_recent/lang/en/block_blog_recent.php new file mode 100644 index 0000000..021c491 --- /dev/null +++ b/blog_recent/lang/en/block_blog_recent.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/>. + +/** + * Strings for component 'block_blog_recent', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_blog_recent + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['blog_recent:addinstance'] = 'Add a new recent blog entries block'; +$string['norecentblogentries'] = 'No recent entries'; +$string['numentriestodisplay'] = 'Number of recent entries to display'; +$string['pluginname'] = 'Recent blog entries'; +$string['recentinterval'] = 'Interval of time considered "recent"'; +$string['privacy:metadata'] = 'The Recent blog entries block only shows data stored in other locations.'; diff --git a/blog_recent/tests/behat/block_blog_recent.feature b/blog_recent/tests/behat/block_blog_recent.feature new file mode 100644 index 0000000..ccb4c64 --- /dev/null +++ b/blog_recent/tests/behat/block_blog_recent.feature @@ -0,0 +1,32 @@ +@block @block_blog_recent +Feature: Feature: Users can use the recent blog entries block to view recent blog entries. + In order to enable the recent blog entries in a course + As a teacher + I can add recent blog entries block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Add the recent blogs block to a course when blogs are disabled + Given I log in as "admin" + And the following config values are set as admin: + | enableblogs | 0 | + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Recent blog entries" block + Then I should see "Blogging is disabled!" in the "Recent blog entries" "block" + + Scenario: Add the recent blogs block to a course when there are not any blog posts + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Recent blog entries" block + Then I should see "No recent entries" in the "Recent blog entries" "block" diff --git a/blog_recent/tests/behat/block_blog_recent_activity.feature b/blog_recent/tests/behat/block_blog_recent_activity.feature new file mode 100644 index 0000000..09112b7 --- /dev/null +++ b/blog_recent/tests/behat/block_blog_recent_activity.feature @@ -0,0 +1,115 @@ +@block @block_blog_menu @mod_assign @block_blog_recent +Feature: Students can use the recent blog entries block to view recent entries on an activity page + In order to enable the recent blog entries block an activity page + As a teacher + I can add the recent blog entries block to an activity page + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + 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 I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Test assignment 1 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + And I follow "Test assignment 1" + And I add the "Blog menu" block + And I add the "Recent blog entries" block + And I log out + + Scenario: Students use the recent blog entries block to view blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I follow "Test assignment 1" + And I should see "S1 First Blog" + And I follow "S1 First Blog" + And I should see "This is my awesome blog!" + + Scenario: Students only see a few entries in the recent blog entries block + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + # Blog 1 of 5 + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + # Blog 2 of 5 + And I set the following fields to these values: + | Entry title | S1 Second Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Second Blog" + And I should see "This is my awesome blog!" + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + # Blog 3 of 5 + And I set the following fields to these values: + | Entry title | S1 Third Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Third Blog" + And I should see "This is my awesome blog!" + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + # Blog 4 of 5 + And I set the following fields to these values: + | Entry title | S1 Fourth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Fourth Blog" + And I should see "This is my awesome blog!" + And I follow "Test assignment 1" + And I follow "Add an entry about this Assignment" + # Blog 5 of 5 + And I set the following fields to these values: + | Entry title | S1 Fifth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I should see "S1 Fifth Blog" + And I should see "This is my awesome blog!" + When I follow "Test assignment 1" + And I should not see "S1 First Blog" + And I should see "S1 Second Blog" + And I should see "S1 Third Blog" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" + And I follow "S1 Fifth Blog" + And I should see "This is my awesome blog!" + Then I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test assignment 1" + And I configure the "Recent blog entries" block + And I set the following fields to these values: + | id_config_numberofrecentblogentries | 2 | + And I press "Save changes" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" diff --git a/blog_recent/tests/behat/block_blog_recent_course.feature b/blog_recent/tests/behat/block_blog_recent_course.feature new file mode 100644 index 0000000..8814cc4 --- /dev/null +++ b/blog_recent/tests/behat/block_blog_recent_course.feature @@ -0,0 +1,105 @@ +@block @block_blog_menu @block_blog_recent +Feature: Students can use the recent blog entries block to view recent entries on a course page + In order to enable the recent blog entries block a course page + As a teacher + I can add the recent blog entries block to a course page + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + 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 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Blog menu" block + And I add the "Recent blog entries" block + And I log out + + Scenario: Students use the recent blog entries block to view blogs + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I should see "S1 First Blog" + And I follow "S1 First Blog" + And I should see "This is my awesome blog!" + + Scenario: Students only see a few entries in the recent blog entries block + Given I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + # Blog 1 of 5 + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + # Blog 2 of 5 + And I set the following fields to these values: + | Entry title | S1 Second Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Second Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + # Blog 3 of 5 + And I set the following fields to these values: + | Entry title | S1 Third Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Third Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + # Blog 4 of 5 + And I set the following fields to these values: + | Entry title | S1 Fourth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Fourth Blog" + And I should see "This is my awesome blog!" + And I am on "Course 1" course homepage + And I follow "Add an entry about this course" + # Blog 5 of 5 + And I set the following fields to these values: + | Entry title | S1 Fifth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I should see "S1 Fifth Blog" + And I should see "This is my awesome blog!" + When I am on "Course 1" course homepage + And I should not see "S1 First Blog" + And I should see "S1 Second Blog" + And I should see "S1 Third Blog" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" + And I follow "S1 Fifth Blog" + And I should see "This is my awesome blog!" + Then I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I configure the "Recent blog entries" block + And I set the following fields to these values: + | id_config_numberofrecentblogentries | 2 | + And I press "Save changes" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" diff --git a/blog_recent/tests/behat/block_blog_recent_frontpage.feature b/blog_recent/tests/behat/block_blog_recent_frontpage.feature new file mode 100644 index 0000000..3b838df --- /dev/null +++ b/blog_recent/tests/behat/block_blog_recent_frontpage.feature @@ -0,0 +1,98 @@ +@block @block_blog_recent +Feature: Feature: Students can use the recent blog entries block to view recent entries on the frontpage + In order to enable the recent blog entries block on the frontpage + As an admin + I can add the recent blog entries block to the frontpage + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Recent blog entries" block + # TODO MDL-57120 site "Blogs" link not accessible without navigation block. + And I add the "Navigation" block if not present + And I log out + + Scenario: Students use the recent blog entries block to view blogs + Given I log in as "student1" + And I am on site homepage + And I navigate to "Site blogs" node in "Site pages" + And I follow "Add a new entry" + When I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + Then I should see "S1 First Blog" + And I should see "This is my awesome blog!" + And I am on site homepage + And I should see "S1 First Blog" + And I follow "S1 First Blog" + And I should see "This is my awesome blog!" + + Scenario: Students only see a few entries in the recent blog entries block + Given I log in as "student1" + And I am on site homepage + And I navigate to "Site blogs" node in "Site pages" + And I follow "Add a new entry" + # Blog 1 of 5 + And I set the following fields to these values: + | Entry title | S1 First Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I follow "Add a new entry" + # Blog 2 of 5 + And I set the following fields to these values: + | Entry title | S1 Second Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Second Blog" + And I should see "This is my awesome blog!" + And I follow "Add a new entry" + # Blog 3 of 5 + And I set the following fields to these values: + | Entry title | S1 Third Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Third Blog" + And I should see "This is my awesome blog!" + And I follow "Add a new entry" + # Blog 4 of 5 + And I set the following fields to these values: + | Entry title | S1 Fourth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I wait "1" seconds + And I should see "S1 Fourth Blog" + And I should see "This is my awesome blog!" + And I follow "Add a new entry" + # Blog 5 of 5 + And I set the following fields to these values: + | Entry title | S1 Fifth Blog | + | Blog entry body | This is my awesome blog! | + And I press "Save changes" + And I should see "S1 Fifth Blog" + And I should see "This is my awesome blog!" + When I am on site homepage + And I should not see "S1 First Blog" + And I should see "S1 Second Blog" + And I should see "S1 Third Blog" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" + And I follow "S1 Fifth Blog" + And I should see "This is my awesome blog!" + Then I log out + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I configure the "Recent blog entries" block + And I set the following fields to these values: + | id_config_numberofrecentblogentries | 2 | + And I press "Save changes" + And I should see "S1 Fourth Blog" + And I should see "S1 Fifth Blog" diff --git a/blog_recent/version.php b/blog_recent/version.php new file mode 100644 index 0000000..60a82e8 --- /dev/null +++ b/blog_recent/version.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/>. + +/** + * Version details + * + * @package block_blog_recent + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_blog_recent'; // Full name of the plugin (used for diagnostics) diff --git a/blog_tags/block_blog_tags.php b/blog_tags/block_blog_tags.php new file mode 100644 index 0000000..f46bd41 --- /dev/null +++ b/blog_tags/block_blog_tags.php @@ -0,0 +1,229 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Blog tags block. + * + * @package block_blog_tags + * @copyright 2006 Shane Elliott + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +define('BLOCK_BLOG_TAGS_DEFAULTTIMEWITHIN', 90); +define('BLOCK_BLOG_TAGS_DEFAULTNUMBEROFTAGS', 20); +define('BLOCK_BLOG_TAGS_DEFAULTSORT', 'name'); + +class block_blog_tags extends block_base { + function init() { + $this->title = get_string('pluginname', 'block_blog_tags'); + } + + function instance_allow_multiple() { + return true; + } + + function has_config() { + return false; + } + + function applicable_formats() { + return array('all' => true, 'my' => false, 'tag' => false); + } + + function instance_allow_config() { + return true; + } + + function specialization() { + + // load userdefined title and make sure it's never empty + if (empty($this->config->title)) { + $this->title = get_string('pluginname', 'block_blog_tags'); + } else { + $this->title = $this->config->title; + } + } + + function get_content() { + global $CFG, $SITE, $USER, $DB, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + + // make sure blog and tags are actually enabled + if (empty($CFG->bloglevel)) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('blogdisable', 'blog'); + } + return $this->content; + + } else if (!core_tag_tag::is_enabled('core', 'post')) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('tagsaredisabled', 'tag'); + } + return $this->content; + + } else if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL and (!isloggedin() or isguestuser())) { + $this->content = new stdClass(); + $this->content->text = ''; + return $this->content; + } + + // require the libs and do the work + require_once($CFG->dirroot .'/blog/lib.php'); + + if (empty($this->config)) { + $this->config = new stdClass(); + } + + if (empty($this->config->timewithin)) { + $this->config->timewithin = BLOCK_BLOG_TAGS_DEFAULTTIMEWITHIN; + } + if (empty($this->config->numberoftags)) { + $this->config->numberoftags = BLOCK_BLOG_TAGS_DEFAULTNUMBEROFTAGS; + } + if (empty($this->config->sort)) { + $this->config->sort = BLOCK_BLOG_TAGS_DEFAULTSORT; + } + + $this->content = new stdClass(); + $this->content->text = ''; + $this->content->footer = ''; + + /// Get a list of tags + $timewithin = time() - $this->config->timewithin * 24 * 60 * 60; /// convert to seconds + + $context = $this->page->context; + + // admins should be able to read all tags + $type = ''; + if (!has_capability('moodle/user:readuserblogs', context_system::instance())) { + $type = " AND (p.publishstate = 'site' or p.publishstate='public')"; + } + + $sql = "SELECT t.id, t.isstandard, t.rawname, t.name, COUNT(DISTINCT ti.id) AS ct + FROM {tag} t, {tag_instance} ti, {post} p, {blog_association} ba + WHERE t.id = ti.tagid AND p.id = ti.itemid + $type + AND ti.itemtype = 'post' + AND ti.component = 'core' + AND ti.timemodified > $timewithin"; + + if ($context->contextlevel == CONTEXT_MODULE) { + $sql .= " AND ba.contextid = $context->id AND p.id = ba.blogid "; + } else if ($context->contextlevel == CONTEXT_COURSE) { + $sql .= " AND ba.contextid = $context->id AND p.id = ba.blogid "; + } + + $sql .= " + GROUP BY t.id, t.isstandard, t.name, t.rawname + ORDER BY ct DESC, t.name ASC"; + + if ($tags = $DB->get_records_sql($sql, null, 0, $this->config->numberoftags)) { + + /// There are 2 things to do: + /// 1. tags with the same count should have the same size class + /// 2. however many tags we have should be spread evenly over the + /// 20 size classes + + $totaltags = count($tags); + $currenttag = 0; + + $size = 20; + $lasttagct = -1; + + $etags = array(); + foreach ($tags as $tag) { + + $currenttag++; + + if ($currenttag == 1) { + $lasttagct = $tag->ct; + $size = 20; + } else if ($tag->ct != $lasttagct) { + $lasttagct = $tag->ct; + $size = 20 - ( (int)((($currenttag - 1) / $totaltags) * 20) ); + } + + $tag->class = ($tag->isstandard ? "standardtag " : "") . "s$size"; + $etags[] = $tag; + + } + + /// Now we sort the tag display order + $CFG->tagsort = $this->config->sort; + usort($etags, "block_blog_tags_sort"); + + /// Finally we create the output + /// Accessibility: markup as a list. + $this->content->text .= "\n<ul class='inline-list'>\n"; + foreach ($etags as $tag) { + $blogurl = new moodle_url('/blog/index.php'); + + switch ($CFG->bloglevel) { + case BLOG_USER_LEVEL: + $blogurl->param('userid', $USER->id); + break; + + default: + if ($context->contextlevel == CONTEXT_MODULE) { + $blogurl->param('modid', $context->instanceid); + } else if ($context->contextlevel == CONTEXT_COURSE) { + $blogurl->param('courseid', $context->instanceid); + } + + break; + } + + $blogurl->param('tagid', $tag->id); + $link = html_writer::link($blogurl, core_tag_tag::make_display_name($tag), + array('class' => $tag->class, + 'title' => get_string('numberofentries', 'blog', $tag->ct))); + $this->content->text .= '<li>' . $link . '</li> '; + } + $this->content->text .= "\n</ul>\n"; + + } + return $this->content; + } +} + +function block_blog_tags_sort($a, $b) { + global $CFG; + + if (empty($CFG->tagsort)) { + return 0; + } else { + $tagsort = $CFG->tagsort; + } + + if (is_numeric($a->$tagsort)) { + return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1; + } elseif (is_string($a->$tagsort)) { + return strcmp($a->$tagsort, $b->$tagsort); //TODO: this is not compatible with UTF-8!! + } else { + return 0; + } +} + + diff --git a/blog_tags/classes/privacy/provider.php b/blog_tags/classes/privacy/provider.php new file mode 100644 index 0000000..8235cd8 --- /dev/null +++ b/blog_tags/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_blog_tags. + * + * @package block_blog_tags + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_blog_tags\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_blog_tags implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/blog_tags/db/access.php b/blog_tags/db/access.php new file mode 100644 index 0000000..a6f6007 --- /dev/null +++ b/blog_tags/db/access.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/>. + +/** + * Blog tags block caps. + * + * @package block_blog_tags + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/blog_tags: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/blog_tags/edit_form.php b/blog_tags/edit_form.php new file mode 100644 index 0000000..581e0b4 --- /dev/null +++ b/blog_tags/edit_form.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/>. + +/** + * Form for editing Blog tags block instances. + * + * @package block_blog_tags + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing Blog tags block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_blog_tags_edit_form extends block_edit_form { + protected function specific_definition($mform) { + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitle', 'block_blog_tags')); + $mform->setDefault('config_title', get_string('blogtags', 'blog')); + $mform->setType('config_title', PARAM_TEXT); + + $numberoftags = array(); + for($i = 1; $i <= 50; $i++) { + $numberoftags[$i] = $i; + } + $mform->addElement('select', 'config_numberoftags', get_string('numberoftags', 'blog'), $numberoftags); + $mform->setDefault('config_numberoftags', BLOCK_BLOG_TAGS_DEFAULTNUMBEROFTAGS); + + $timewithin = array( + 10 => get_string('numdays', '', 10), + 30 => get_string('numdays', '', 30), + 60 => get_string('numdays', '', 60), + 90 => get_string('numdays', '', 90), + 120 => get_string('numdays', '', 120), + 240 => get_string('numdays', '', 240), + 365 => get_string('numdays', '', 365), + ); + $mform->addElement('select', 'config_timewithin', get_string('timewithin', 'blog'), $timewithin); + $mform->setDefault('config_timewithin', BLOCK_BLOG_TAGS_DEFAULTTIMEWITHIN); + + $sort = array( + 'name' => get_string('tagtext', 'blog'), + 'id' => get_string('tagdatelastused', 'blog'), + ); + $mform->addElement('select', 'config_sort', get_string('tagsort', 'blog'), $sort); + $mform->setDefault('config_sort', BLOCK_BLOG_TAGS_DEFAULTSORT); + } +} diff --git a/blog_tags/lang/en/block_blog_tags.php b/blog_tags/lang/en/block_blog_tags.php new file mode 100644 index 0000000..298ffe6 --- /dev/null +++ b/blog_tags/lang/en/block_blog_tags.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_blog_tags', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_blog_tags + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['blog_tags:addinstance'] = 'Add a new blog tags block'; +$string['pluginname'] = 'Blog tags'; +$string['configtitle'] = 'Blog tags block title'; +$string['privacy:metadata'] = 'The Blog tags block only shows data stored in other locations.'; diff --git a/blog_tags/styles.css b/blog_tags/styles.css new file mode 100644 index 0000000..9fc8a17 --- /dev/null +++ b/blog_tags/styles.css @@ -0,0 +1,68 @@ +.block_blog_tags .s20 { + font-size: 1.5em; + font-weight: bold; +} + +.block_blog_tags .s19 { + font-size: 1.5em; +} + +.block_blog_tags .s18 { + font-size: 1.4em; + font-weight: bold; +} + +.block_blog_tags .s17 { + font-size: 1.4em; +} + +.block_blog_tags .s16 { + font-size: 1.3em; + font-weight: bold; +} + +.block_blog_tags .s15 { + font-size: 1.3em; +} + +.block_blog_tags .s14 { + font-size: 1.2em; + font-weight: bold; +} + +.block_blog_tags .s13 { + font-size: 1.2em; +} + +.block_blog_tags .s12, +.block_blog_tags .s11 { + font-size: 1.1em; + font-weight: bold; +} + +.block_blog_tags .s10, +.block_blog_tags .s9 { + font-size: 1.1em; +} + +.block_blog_tags .s8, +.block_blog_tags .s7 { + font-size: 1em; + font-weight: bold; +} + +.block_blog_tags .s6, +.block_blog_tags .s5 { + font-size: 1em; +} + +.block_blog_tags .s4, +.block_blog_tags .s3 { + font-size: 0.9em; + font-weight: bold; +} + +.block_blog_tags .s2, +.block_blog_tags .s1 { + font-size: 0.9em; +} \ No newline at end of file diff --git a/blog_tags/tests/behat/blogtag.feature b/blog_tags/tests/behat/blogtag.feature new file mode 100644 index 0000000..8cf6b27 --- /dev/null +++ b/blog_tags/tests/behat/blogtag.feature @@ -0,0 +1,55 @@ +@block @block_blog_tag @core_blog @core_tag +Feature: Adding blog tag block + In order to search blog post by tag + As a user + I need to be able to use block blog tag + + @javascript + Scenario: Adding block blog tag to the course + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | c1 | + And the following "tags" exist: + | name | isstandard | + | Neverusedtag | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | c1 | editingteacher | + | student1 | c1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Blog tags" block + # TODO MDL-57120 site "Blogs" link not accessible without navigation block. + And I add the "Navigation" block if not present + + And I navigate to course participants + And I click on "Course blogs" "link" in the "Navigation" "block" + And I follow "Blog about this Course" + And I set the following fields to these values: + | Entry title | Blog post from teacher | + | Blog entry body | Teacher blog post content | + | Tags | Cats, dogs | + And I press "Save changes" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I navigate to course participants + And I click on "Course blogs" "link" in the "Navigation" "block" + And I follow "Blog about this Course" + And I set the following fields to these values: + | Entry title | Blog post from student | + | Blog entry body | Student blog post content | + | Tags | dogs, mice | + And I press "Save changes" + And I follow "c1" + Then I should see "Cats" in the "Blog tags" "block" + And I should see "dogs" in the "Blog tags" "block" + And I should see "mice" in the "Blog tags" "block" + And I click on "Cats" "link" in the "Blog tags" "block" + And I should see "Blog post from teacher" + And I should see "Teacher blog post content" + And I log out diff --git a/blog_tags/version.php b/blog_tags/version.php new file mode 100644 index 0000000..af3a7c5 --- /dev/null +++ b/blog_tags/version.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/>. + +/** + * Version details + * + * @package block_blog_tags + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_blog_tags'; // Full name of the plugin (used for diagnostics) diff --git a/calendar_month/block_calendar_month.php b/calendar_month/block_calendar_month.php new file mode 100644 index 0000000..00547c6 --- /dev/null +++ b/calendar_month/block_calendar_month.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/>. + +/** + * Handles displaying the calendar block. + * + * @package block_calendar_month + * @copyright 2004 Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_calendar_month extends block_base { + + /** + * Initialise the block. + */ + public function init() { + $this->title = get_string('pluginname', 'block_calendar_month'); + } + + /** + * Return the content of this block. + * + * @return stdClass the content + */ + public function get_content() { + global $CFG; + + require_once($CFG->dirroot.'/calendar/lib.php'); + + if ($this->content !== null) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + $courseid = $this->page->course->id; + $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null; + $calendar = \calendar_information::create(time(), $courseid, $categoryid); + list($data, $template) = calendar_get_view($calendar, 'mini', isloggedin(), isloggedin()); + + $renderer = $this->page->get_renderer('core_calendar'); + $this->content->text .= $renderer->render_from_template($template, $data); + + if ($this->page->course->id != SITEID) { + $this->content->text .= $renderer->event_filter(); + } + + return $this->content; + } +} diff --git a/calendar_month/classes/privacy/provider.php b/calendar_month/classes/privacy/provider.php new file mode 100644 index 0000000..0ff00af --- /dev/null +++ b/calendar_month/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_calendar_month. + * + * @package block_calendar_month + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_calendar_month\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_calendar_month implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/calendar_month/db/access.php b/calendar_month/db/access.php new file mode 100644 index 0000000..71cb0bc --- /dev/null +++ b/calendar_month/db/access.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/>. + +/** + * Calendar month block caps. + * + * @package block_calendar_month + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/calendar_month:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/calendar_month: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/calendar_month/db/upgrade.php b/calendar_month/db/upgrade.php new file mode 100644 index 0000000..4403a77 --- /dev/null +++ b/calendar_month/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/>. + +/** + * This file keeps track of upgrades to the calendar_month block + * + * 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. + * + * @since Moodle 2.8 + * @package block_calendar_month + * @copyright 2014 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade the calendar_month block + * @param int $oldversion + * @param object $block + */ +function xmldb_block_calendar_month_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/calendar_month/lang/en/block_calendar_month.php b/calendar_month/lang/en/block_calendar_month.php new file mode 100644 index 0000000..38aeda7 --- /dev/null +++ b/calendar_month/lang/en/block_calendar_month.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_calendar_month', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_calendar_month + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['calendar_month:addinstance'] = 'Add a new calendar block'; +$string['calendar_month:myaddinstance'] = 'Add a new calendar block to Dashboard'; +$string['pluginname'] = 'Calendar'; +$string['privacy:metadata'] = 'The Calendar block only displays existing calendar data.'; diff --git a/calendar_month/tests/behat/block_calendar_month.feature b/calendar_month/tests/behat/block_calendar_month.feature new file mode 100644 index 0000000..bc34346 --- /dev/null +++ b/calendar_month/tests/behat/block_calendar_month.feature @@ -0,0 +1,195 @@ +@block @block_calendar_month +Feature: Enable the calendar block in a course and test it's functionality + In order to enable the calendar block in a course + As a teacher + I can add the calendar block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + 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 | + + Scenario: Add the block to a the course + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Calendar" block + Then "Calendar" "block" should exist + + @javascript + Scenario: View a global event in the calendar block + Given I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | Site Event | + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I hover over today in the calendar + Then I should see "Site Event" + + @javascript + Scenario: Filter site events in the calendar block + Given I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | Site Event | + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Course | + | id_name | Course Event | + And I am on "Course 1" course homepage + And I follow "Hide global events" + And I hover over today in the calendar + Then I should not see "Site Event" + And I should see "Course Event" + + @javascript + Scenario: View a course event in the calendar block + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Course | + | id_name | Course Event | + When I am on "Course 1" course homepage + And I hover over today in the calendar + Then I should see "Course Event" + + @javascript + Scenario: Filter course events in the calendar block + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Course | + | id_name | Course Event | + And I am on "Course 1" course homepage + And I create a calendar event with form data: + | id_eventtype | User | + | id_name | User Event | + And I am on "Course 1" course homepage + And I follow "Hide course events" + And I hover over today in the calendar + Then I should not see "Course Event" + And I should see "User Event" + + @javascript + Scenario: View a user event in the calendar block + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | User | + | id_name | User Event | + When I am on "Course 1" course homepage + And I hover over today in the calendar + Then I should see "User Event" + + @javascript + Scenario: Filter user events in the calendar block + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Course | + | id_name | Course Event | + And I am on "Course 1" course homepage + And I create a calendar event with form data: + | id_eventtype | User | + | id_name | User Event | + When I am on "Course 1" course homepage + And I follow "Hide user events" + And I hover over today in the calendar + Then I should not see "User Event" + And I should see "Course Event" + + @javascript + Scenario: View a group event in the calendar block + Given 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 | + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" node in "Course administration" + And I set the following fields to these values: + | id_groupmode | Separate groups | + | id_groupmodeforce | Yes | + And I press "Save and display" + And I turn editing mode on + And I add the "Calendar" block + And I click on "This month" "link" + And I click on "New event" "button" + And I set the following fields to these values: + | id_eventtype | Group | + | id_name | Group Event | + And I set the following fields to these values: + | Group | Group 1 | + And I press "Save" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I hover over today in the calendar + And I should see "Group Event" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I hover over today in the calendar + And I should not see "Group Event" + + @javascript + Scenario: Filter group events in the calendar block + Given 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 | + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" node in "Course administration" + And I set the following fields to these values: + | id_groupmode | Separate groups | + | id_groupmodeforce | Yes | + And I press "Save and display" + And I turn editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Course | + | id_name | Course Event 1 | + And I am on "Course 1" course homepage + And I click on "This month" "link" + And I click on "New event" "button" + And I set the following fields to these values: + | id_eventtype | Group | + | id_name | Group Event 1 | + And I set the following fields to these values: + | Group | Group 1 | + And I press "Save" + And I log out + Then I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Hide group events" + And I hover over today in the calendar + And I should not see "Group Event 1" + And I should see "Course Event 1" diff --git a/calendar_month/tests/behat/block_calendar_month_course.feature b/calendar_month/tests/behat/block_calendar_month_course.feature new file mode 100644 index 0000000..2360572 --- /dev/null +++ b/calendar_month/tests/behat/block_calendar_month_course.feature @@ -0,0 +1,27 @@ +@block @block_calendar_month +Feature: Enable the calendar block in a course + In order to enable the calendar block in a course + As a teacher + I can add the calendar block to a course + + @javascript + Scenario: View a global event in the calendar block in a course + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + When I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | Site Event | + And I log out + Then I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Calendar" block + And I hover over today in the calendar + And I should see "Site Event" diff --git a/calendar_month/tests/behat/block_calendar_month_dashboard.feature b/calendar_month/tests/behat/block_calendar_month_dashboard.feature new file mode 100644 index 0000000..aa74c43 --- /dev/null +++ b/calendar_month/tests/behat/block_calendar_month_dashboard.feature @@ -0,0 +1,19 @@ +@block @block_calendar_month +Feature: View a site event on the dashboard + In order to view a site event + As a student + I can view the event in the calendar + + @javascript + Scenario: View a global event in the calendar block on the dashboard + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | Site Event | + And I log out + When I log in as "student1" + And I hover over today in the calendar + Then I should see "Site Event" diff --git a/calendar_month/tests/behat/block_calendar_month_frontpage.feature b/calendar_month/tests/behat/block_calendar_month_frontpage.feature new file mode 100644 index 0000000..b7ef68e --- /dev/null +++ b/calendar_month/tests/behat/block_calendar_month_frontpage.feature @@ -0,0 +1,23 @@ +@block @block_calendar_month +Feature: Enable the calendar block on the site front page + In order to enable the calendar block on the site front page + As an admin + I can add the calendar block on the site front page + + @javascript + Scenario: View a global event in the calendar block on the front page + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Calendar" block + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | Site Event | + And I log out + When I log in as "student1" + And I am on site homepage + And I hover over today in the calendar + Then I should see "Site Event" diff --git a/calendar_month/version.php b/calendar_month/version.php new file mode 100644 index 0000000..b91baa4 --- /dev/null +++ b/calendar_month/version.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/>. + +/** + * Version details + * + * @package block_calendar_month + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_calendar_month'; // Full name of the plugin (used for diagnostics) diff --git a/calendar_upcoming/block_calendar_upcoming.php b/calendar_upcoming/block_calendar_upcoming.php new file mode 100644 index 0000000..137ba26 --- /dev/null +++ b/calendar_upcoming/block_calendar_upcoming.php @@ -0,0 +1,127 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 displaying the calendar upcoming events block. + * + * @package block_calendar_upcoming + * @copyright 2004 Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_calendar_upcoming extends block_base { + + /** + * Initialise the block. + */ + public function init() { + $this->title = get_string('pluginname', 'block_calendar_upcoming'); + } + + /** + * Return the content of this block. + * + * @return stdClass the content + */ + public function get_content() { + global $CFG; + + require_once($CFG->dirroot.'/calendar/lib.php'); + + if ($this->content !== null) { + return $this->content; + } + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + $courseid = $this->page->course->id; + $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null; + $calendar = \calendar_information::create(time(), $courseid, $categoryid); + list($data, $template) = calendar_get_view($calendar, 'upcoming_mini'); + + $renderer = $this->page->get_renderer('core_calendar'); + $this->content->text .= $renderer->render_from_template($template, $data); + + $url = new \moodle_url('/calendar/view.php', ['view' => 'upcoming']); + if ($courseid != SITEID) { + $url->param('course', $this->page->course->id); + } else if (!empty($categoryid)) { + $url->param('category', $this->page->category->id); + } + + $this->content->footer = html_writer::div( + html_writer::link($url, get_string('gotocalendar', 'block_calendar_upcoming')), + 'gotocal' + ); + + return $this->content; + } + + /** + * Get the upcoming event block content. + * + * @param array $events list of events + * @param \moodle_url|string $linkhref link to event referer + * @param boolean $showcourselink whether links to courses should be shown + * @return string|null $content html block content + * @deprecated since 3.4 + */ + public static function get_upcoming_content($events, $linkhref = null, $showcourselink = false) { + debugging( + 'get_upcoming_content() is deprecated. ' . + 'Please see block_calendar_upcoming::get_content() for the correct API usage.', + DEBUG_DEVELOPER + ); + + $content = ''; + $lines = count($events); + + if (!$lines) { + return $content; + } + + for ($i = 0; $i < $lines; ++$i) { + if (!isset($events[$i]->time)) { + continue; + } + $events[$i] = calendar_add_event_metadata($events[$i]); + $content .= '<div class="event"><span class="icon c0">' . $events[$i]->icon . '</span>'; + if (!empty($events[$i]->referer)) { + // That's an activity event, so let's provide the hyperlink. + $content .= $events[$i]->referer; + } else { + if (!empty($linkhref)) { + $href = calendar_get_link_href(new \moodle_url(CALENDAR_URL . $linkhref), 0, 0, 0, + $events[$i]->timestart); + $href->set_anchor('event_' . $events[$i]->id); + $content .= \html_writer::link($href, $events[$i]->name); + } else { + $content .= $events[$i]->name; + } + } + $events[$i]->time = str_replace('»', '<br />»', $events[$i]->time); + if ($showcourselink && !empty($events[$i]->courselink)) { + $content .= \html_writer::div($events[$i]->courselink, 'course'); + } + $content .= '<div class="date">' . $events[$i]->time . '</div></div>'; + if ($i < $lines - 1) { + $content .= '<hr />'; + } + } + + return $content; + } +} diff --git a/calendar_upcoming/classes/privacy/provider.php b/calendar_upcoming/classes/privacy/provider.php new file mode 100644 index 0000000..ae4f01a --- /dev/null +++ b/calendar_upcoming/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_calendar_upcoming. + * + * @package block_calendar_upcoming + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_calendar_upcoming\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_calendar_upcoming implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/calendar_upcoming/db/access.php b/calendar_upcoming/db/access.php new file mode 100644 index 0000000..673cf8b --- /dev/null +++ b/calendar_upcoming/db/access.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/>. + +/** + * Calendar upcoming block caps. + * + * @package block_calendar_upcoming + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/calendar_upcoming:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/calendar_upcoming: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/calendar_upcoming/db/upgrade.php b/calendar_upcoming/db/upgrade.php new file mode 100644 index 0000000..dbe19a5 --- /dev/null +++ b/calendar_upcoming/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/>. + +/** + * This file keeps track of upgrades to the calendar_upcoming block + * + * 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. + * + * @since Moodle 2.8 + * @package block_calendar_upcoming + * @copyright 2014 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade the calendar_upcoming block + * @param int $oldversion + * @param object $block + */ +function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/calendar_upcoming/lang/en/block_calendar_upcoming.php b/calendar_upcoming/lang/en/block_calendar_upcoming.php new file mode 100644 index 0000000..0b951f3 --- /dev/null +++ b/calendar_upcoming/lang/en/block_calendar_upcoming.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/>. + +/** + * Strings for component 'block_calendar_upcoming', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_calendar_upcoming + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['calendar_upcoming:addinstance'] = 'Add a new upcoming events block'; +$string['calendar_upcoming:myaddinstance'] = 'Add a new upcoming events block to Dashboard'; +$string['gotocalendar'] = 'Go to calendar...'; +$string['pluginname'] = 'Upcoming events'; +$string['privacy:metadata'] = 'The Upcoming events block only displays existing calendar data.'; diff --git a/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature b/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature new file mode 100644 index 0000000..9e092bf --- /dev/null +++ b/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature @@ -0,0 +1,26 @@ +@block @block_calendar_upcoming +Feature: Enable the upcoming events block in a course + In order to enable the calendar block in a course + As a teacher + I can view the event in the upcoming events block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + @javascript + Scenario: View a global event in the calendar block + Given I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | My Site Event | + And I log out + When I log in as "teacher1" + Then I should see "My Site Event" in the "Upcoming events" "block" diff --git a/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature b/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature new file mode 100644 index 0000000..0b7c739 --- /dev/null +++ b/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature @@ -0,0 +1,19 @@ +@block @block_calendar_upcoming +Feature: View a upcoming site event on the dashboard + In order to view a site event + As a student + I can view the event in the upcoming events block + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + + @javascript + Scenario: View a global event in the upcoming events block on the dashboard + Given I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | My Site Event | + And I log out + When I log in as "student1" + Then I should see "My Site Event" diff --git a/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature b/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature new file mode 100644 index 0000000..448b710 --- /dev/null +++ b/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature @@ -0,0 +1,24 @@ +@block @block_calendar_upcoming +Feature: View a site event on the frontpage + In order to view a site event + As a teacher + I can view the event in the upcoming events block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + + @javascript + Scenario: View a global event in the upcoming events block on the frontpage + Given I log in as "admin" + And I create a calendar event with form data: + | id_eventtype | Site | + | id_name | My Site Event | + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Upcoming events" block + And I log out + When I log in as "teacher1" + And I am on site homepage + Then I should see "My Site Event" in the "Upcoming events" "block" diff --git a/calendar_upcoming/upgrade.txt b/calendar_upcoming/upgrade.txt new file mode 100644 index 0000000..b5c14a3 --- /dev/null +++ b/calendar_upcoming/upgrade.txt @@ -0,0 +1,5 @@ +=== 3.4 === + +* block_calendar_upcoming::get_upcoming_content has been deprecated. Please + update your code to use the new APIs. You can see an example of how these + may be used in block_calendar_upcoming::get_content(). diff --git a/calendar_upcoming/version.php b/calendar_upcoming/version.php new file mode 100644 index 0000000..bbd4e43 --- /dev/null +++ b/calendar_upcoming/version.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/>. + +/** + * Version details + * + * @package block_calendar_upcoming + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_calendar_upcoming'; // Full name of the plugin (used for diagnostics) diff --git a/career/README.md b/career/README.md new file mode 100644 index 0000000..f6614c0 --- /dev/null +++ b/career/README.md @@ -0,0 +1,49 @@ +# Bloc parcours + +Plugin MOODLE de type bloc de cours qui permet d'afficher une liste de certaines activités existante du cours sur une page à part. P.ex. pour créer des parcours de niveaux, thématiques, etc. dans un cours volumineux. + +## Auteurs + +- Vrignaud Camille <cvrignaud@softia.fr> +- Lebeau Michaël <mlebeau@softia.fr> +- Thomas Fradet <thomas.fradet@univ-lorraine.fr> + +## Compatibility + +MOODLE 3.5 + +Stabilité : expérimental. + +## Contribution + +Contributors are welcom ! Please contact <iena-contact@univ-lorraine.fr>. + +## Contact + +Pour assistance interne (Univesité de Lorraine) : <https://helpdesk.univ-lorraine.fr>. + +Pour tout autre question : <iena-contact@univ-lorraine.fr>. + +Other : <iena-contact@univ-lorraine.fr>. + +## Fonctionnalités + +L'enseignant ajoute un parcours en lui donnant un nom et en sélectionnant plusieurs ressources ou activités du cours. + +L'étudiant voit une liste des parcours dans le bloc. En cliquant sur le nom d'un parcours, il voit une page qui lui présente la liste des ressoures et activités du cours choisies par l'enseignant pour ce parcours. + +--- + +Teacher add a path with name and ressources and activity subset / selection from course. + +Student see path list quand by clicking one of them, can access to the list of ressources and activity choosen by the teacher for this learning path. + +## Problèmes connus + +- Affichage peu élégant de la liste des ressources et activités. +- Pas de possibilité de passer d'un élément à l'autre du parcours sans revenir à la page du parcours. + +## Amélioration à effectuer + +- Améliorer l'apparence de la liste des activités et ressources d'un parcours. +- Permettre de consulter les éléments d'un parcours avec un meilleur enchaînement. diff --git a/career/block_career.php b/career/block_career.php new file mode 100644 index 0000000..f839e5b --- /dev/null +++ b/career/block_career.php @@ -0,0 +1,109 @@ +<?php + +/** + * block_career + * + * + * @package block_career + * @category block + * @copyright 2018 Softia/Université lorraine + * @author vrignaud camille/ faouzi / Thomas Fradet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_career extends block_base +{ + /** + * + */ + public function init() + { + $this->title = get_string('title_plugin', 'block_career'); + } + + /** + * @return bool + */ + function instance_allow_multiple() + { + return false; + } + + /** + * Set the applicable formats for this block to all + * @return array + */ + function applicable_formats() + { + return array('course' => true); + } + + /** + * Allow the user to configure a block instance + * @return bool Returns true + */ + function instance_allow_config() + { + + } + + /** + * @return stdClass + */ + public function get_content() + { + global $CFG; + global $COURSE; + global $DB; + + if ($this->content !== null) { + return $this->content; + } + if (empty($this->config)) { + $this->config = new stdClass(); + } + + $request = $DB->get_records_sql('SELECT * FROM {block_career} WHERE course = ?', array($COURSE->id)); + // var_dump($request); + $careerId = optional_param("career", NULL, PARAM_INT); + $active = ""; + + $this->content = new stdClass; + $this->content->text .= '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $COURSE->id . '" class="btn btn-success btn-career-block mb-3">Accueil du cours</a>'; + + // $image = ""; + + $this->content->text .= '<div class="list-group">'; + + foreach ($request as $value) { + + // if (file_get_contents("$CFG->wwwroot/blocks/career/$value->image") != null) { + // $image = "<img src='$CFG->wwwroot/blocks/career/$value->image' class='img_moodle_list'/>"; + // } + + if ($careerId != null && $careerId == $value->id) { + $active = "active"; + } else { + $active = ""; + } + + $this->content->text .= "<a href='" . $CFG->wwwroot . "/blocks/career/career_unit.php?career=" . $value->id . "' class='list-group-item $active'>$value->name</a>"; + // $this->content->text .= "<a href='" . $CFG->wwwroot . "/blocks/career/career_unit.php?career=" . $value->id . "' class='full list-group-item list-group-item-action $active'><div class=' left img_center'>$image</div> + //    $value->name</a><br>"; + } + + $this->content->text .= '</div>'; + + if (empty($request)) { + $this->content->text .= "<p>" . get_string('any_carrer', 'block_career') . "</p>"; + } + + $this->content->text .= '<a href="' . $CFG->wwwroot . '/blocks/career/career_list.php?course=' . $COURSE->id . '" type="button " class="btn btn-primary btn-career-block mt-3">Gérer les parcours</a>'; + + // $this->content->text .= "<p></p>"; + + return $this->content; + } +} + +?> \ No newline at end of file diff --git a/career/career_list.php b/career/career_list.php new file mode 100644 index 0000000..1617256 --- /dev/null +++ b/career/career_list.php @@ -0,0 +1,38 @@ +<?php + define('NO_OUTPUT_BUFFERING', true); + require_once('../../config.php'); + require_once('entity/block_career_ressource.php'); + require_once('entity/block_career_section.php'); + require_once('view/view_career_list.php'); + + global $COURSE; + global $USER; + global $DB; + global $CFG; + require_once($CFG->libdir . '/adminlib.php'); + + $id_course = required_param('course', PARAM_INT); + + $url = new moodle_url('/blocks/career/career_list.php', array('course' => $id_course)); + //Check if the user has capability to update course + if (!has_capability('moodle/course:update', $context = context_course::instance($id_course), $USER->id)) { + header("Location: {$_SERVER['HTTP_REFERER']}"); + exit; + } + + $PAGE->set_url($url); + $PAGE->set_pagelayout('admin'); + + $course = $DB->get_record('course', array('id' => $id_course), '*', MUST_EXIST); + require_login($course, false, NULL); + + $PAGE->set_title(get_string('title_plugin', 'block_career')); + $PAGE->set_heading($OUTPUT->heading($COURSE->fullname, 2, 'headingblock header outline')); + + $ressource = new block_career_ressource(); + $section = new block_career_section(); + echo $OUTPUT->header(); + echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\">"; + $content = new view_career_list(); + echo $content->get_content(); + echo $OUTPUT->footer(); \ No newline at end of file diff --git a/career/career_setting.php b/career/career_setting.php new file mode 100644 index 0000000..d0bc22a --- /dev/null +++ b/career/career_setting.php @@ -0,0 +1,82 @@ +<?php + + ob_start(); + + require_once('../../config.php'); + global $COURSE, $DB, $CFG; + require_once("$CFG->libdir/formslib.php"); + require_once('entity/block_career_ressource.php'); + require_once('entity/block_career_section.php'); + require_once('view/view_career_setting.php'); + + $id_course = required_param('course', PARAM_INT); + $url = new moodle_url('/blocks/career/career_setting.php', array('course' => $id_course)); + + $PAGE->set_pagelayout('course'); + $PAGE->set_url($url); + + $course = $DB->get_record('course', array('id' => $id_course), '*', MUST_EXIST); + require_login($course, false, NULL); + + + $PAGE->set_title(get_string('title_plugin', 'block_career')); + $PAGE->set_heading($OUTPUT->heading($COURSE->fullname, 2, 'headingblock header outline')); + echo $OUTPUT->header(); + $PAGE->requires->js("/blocks/career/js/jquery.min.js"); + $PAGE->requires->js("/blocks/career/js/file.js"); + // echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\">"; + + $content = new view_career_setting(); + echo $content->get_content(); + + // Delete career + if (isset($_GET["delete"]) && $_GET["delete"] == 1) { + $DB->execute("DELETE FROM {block_career} WHERE id = ?", array($_GET["id"])); + header("Location: $CFG->wwwroot/blocks/career/career_list.php?course=" . $_GET["course"]); + } + + if (!empty($_POST["careerName"])) { + + $ressourses = ""; + + foreach ($_POST["ressource"] as $value) { + if ($value === end($_POST["ressource"])) { + $ressourses .= "$value"; + } else { + $ressourses .= "$value,"; + } + } + + //$record is use for insert/update in database + $record = new stdClass(); + $record->course = intval($_GET["course"]); + $record->name = $_POST["careerName"]; + $record->description = $_POST["descriptionName"]["text"]; + + // if (isset($_FILES['imageName']['tmp_name'])) { + // $pathDir = "img/"; + // $pathFile = $pathDir . basename($_FILES["imageName"]["name"]); + // move_uploaded_file($_FILES['imageName']['tmp_name'], $pathFile); + // $record->image = $pathFile; + // } else { + // $record->image = $_POST["imagePath"]; + // } + $record->image = ""; + + $record->ressources = $ressourses; + + if ($_POST["careerId"] != 0) { + $record->id = intval($_POST["careerId"]); + $lastinsertid = $DB->update_record('block_career', $record); + } else { + $lastinsertid = $DB->insert_record('block_career', $record); + } + + if ($lastinsertid != 0) { + header("Location: $CFG->wwwroot/blocks/career/career_list.php?course=" . $_GET["course"]); + } + + } + + + echo $OUTPUT->footer(); diff --git a/career/career_unit.php b/career/career_unit.php new file mode 100644 index 0000000..ee633e5 --- /dev/null +++ b/career/career_unit.php @@ -0,0 +1,30 @@ +<?php + + require_once('../../config.php'); + require_once('entity/block_career_ressource.php'); + require_once('entity/block_career_section.php'); + + global $COURSE, $DB; + + $careerId = required_param('career', PARAM_INT); + $url = new moodle_url('/blocks/career/career_unit.php', array('career' => $careerId)); + $requete = $DB->get_record_sql('SELECT course FROM {block_career} WHERE id = ?', array($careerId)); + + $PAGE->set_pagelayout('course'); + $PAGE->set_url($url); + + // Getting DB data for the course + $course = $DB->get_record('course', array('id' => $requete->course), '*', MUST_EXIST); + // Must be logged in + require_login($course, false, NULL); + + $PAGE->set_title(get_string('title_plugin', 'block_career')); + $PAGE->set_heading($OUTPUT->heading($COURSE->fullname, 2, 'headingblock header outline')); + echo $OUTPUT->header(); + $PAGE->requires->js("/blocks/career/js/jquery.min.js"); + $PAGE->requires->js("/blocks/career/js/file.js"); + echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\">"; + + require_once('view/view_career_unit.php'); + + echo $OUTPUT->footer(); diff --git a/career/db/access.php b/career/db/access.php new file mode 100644 index 0000000..f1dfeb2 --- /dev/null +++ b/career/db/access.php @@ -0,0 +1,25 @@ +<?php +$capabilities = array( + + 'block/career:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/career:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), +); +?> \ No newline at end of file diff --git a/career/db/install.xml b/career/db/install.xml new file mode 100644 index 0000000..2ec15b6 --- /dev/null +++ b/career/db/install.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="blocks/career/db" VERSION="20180212" COMMENT="XMLDB file for Moodle blocks/career" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="block_career" COMMENT="Table du block parcours"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="course" TYPE="int" LENGTH="8" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="name" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="ressources" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="image" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/career/edit_form.php b/career/edit_form.php new file mode 100644 index 0000000..e0a49d0 --- /dev/null +++ b/career/edit_form.php @@ -0,0 +1,20 @@ +<?php + if (!defined('MOODLE_INTERNAL')) + die('Direct access to this script is forbidden.'); + + class block_career_edit_form extends block_edit_form + { + + /** + * @param $mform + */ + function specific_definition($mform) + { + // Adding an element to the form + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + } + } + +?> + + diff --git a/career/entity/block_career_ressource.php b/career/entity/block_career_ressource.php new file mode 100644 index 0000000..3a20667 --- /dev/null +++ b/career/entity/block_career_ressource.php @@ -0,0 +1,108 @@ +<?php + /** + * The iena filter plugin transforms the moodle resource links + * into a button that opens the resource in a modal + * + * @package block_career + * @category block + * @copyright 2018 Softia/Université lorraine + * @author vrignaud camille/ faouzi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + /** + * block_career_ressource + * + * + * @package filter_iena + * @copyright 2018 Softia/Université lorraine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + class block_career_ressource + { + + /** @var int Id of ressource */ + public $id; + /** @var string name of ressource */ + public $name; + /** @var string link of ressource */ + public $link; + /** @var string type of ressource */ + public $type; + /** @var int module id of ressource */ + public $module; + /** @var string intro of ressource */ + public $descrition; + /** @var block_career_section section of ressource */ + public $section; + + /** + * Set $id, $name, $type + * @param array $id_course_modules + * + * @return void + */ + public function get_ressource_by_id($id_course_modules) + { + + global $DB; + if ($id_course_modules) { + $this->id = $id_course_modules; + $requete = $DB->get_record_sql('SELECT * FROM {course_modules} WHERE id = ? AND deletioninprogress = 0', array($id_course_modules)); + $id_instance = $requete->instance; + $id_module = $requete->module; + if ($id_module) { + $modules = $DB->get_record_sql('SELECT * FROM {modules} WHERE id = ?', array($id_module)); + } + if ($modules->name) { + $instance = $DB->get_record_sql('SELECT * FROM {' . $modules->name . '} WHERE id = ?', array($id_instance)); + } + if ($instance->name) { + $this->name = $instance->name; + } + $this->descrition = $instance->intro; + $this->type = $modules->name; + $this->module = $modules->id; + $this->section = new block_career_section(); + $this->section->get_section_by_id_section($requete->section); + $this->create_link(); + } + } + + /** + * Get all ressources in a section + * return a array + * @param array $id_section + * + * @return array<block_career_ressource> $ressources + */ + public function get_ressources_by_id_section($id_section) + { + global $DB; + $requete = $DB->get_records_sql('SELECT id FROM {course_modules} WHERE section = ? AND deletioninprogress = 0', array($id_section)); + $ressources = array(); + $i = 0; + foreach ($requete as $value) { + $ressource = new block_career_ressource(); + $ressource->get_ressource_by_id($value->id); + $ressources[$i] = $ressource; + $i++; + } + + return $ressources; + } + + /** + * Create and SET ($link) a correct link with $CFG->wwwroot, $type and $id + * @param void + * + * @return void + */ + private function create_link() + { + + global $CFG; + $this->link = $CFG->wwwroot . '/mod/' . $this->type . '/view.php?id=' . $this->id; + } + + } diff --git a/career/entity/block_career_section.php b/career/entity/block_career_section.php new file mode 100644 index 0000000..a230abb --- /dev/null +++ b/career/entity/block_career_section.php @@ -0,0 +1,91 @@ +<?php + + /** + * block_career_section + * + * + * @package block_career + * @category block + * @copyright 2018 Softia/Université lorraine + * @author vrignaud camille/ faouzi / Thomas Fradet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + class block_career_section + { + + /** @var int Id of section */ + public $id; + /** @var string name of section */ + public $name; + /** @var int id of course */ + public $id_course; + /** @var block_career_ressources array<Object> ressources */ + public $ressources; + /** @var string summary */ + public $summary; + /** @var int order of section */ + public $orde; + /** @var string intro of section */ + public $intro; + + /** + * Set $id, $name, $id_course and $summary + * @param array $id_section + * + * @return void + */ + public function get_section_by_id_section($id_section) + { + global $DB; + $requete = $DB->get_record_sql('SELECT * FROM {course_sections} WHERE id = ?', array($id_section)); + $this->id = $requete->id; + $this->name = $requete->name; + $this->id_course = $requete->course; + $this->summary = $requete->summary; + $this->orde = $requete->section; + $this->intro = $requete->summary; + + if (!$this->name) { + $this->name = "Section " . $requete->section; + } + } + + /** + * Get all sections in a course + * return a array + * @param array $id_course + * + * @return array<block_career_section> $sections + */ + public function get_sections_by_id_course($id_course) + { + + global $DB; + $requete = $DB->get_records_sql('SELECT * FROM {course_sections} WHERE course = ? ORDER BY section', array($id_course)); + // $numsection = $DB->get_record_sql('SELECT value FROM {course_format_options} WHERE courseid = ? AND name = ?', array($id_course,"numsections")); + $sections = array(); + $i = 0; + foreach ($requete as $value) { + // if ($value->section > $numsection->value){ + // continue; + // } + $section = new block_career_section(); + $section->get_section_by_id_section($value->id); + $sections[$i] = $section; + $i++; + } + + return $sections; + + } + + + + + + + + + + } diff --git "a/career/img/Capture d\342\200\231\303\251cran 2018-06-19 \303\240 18.18.32.png" "b/career/img/Capture d\342\200\231\303\251cran 2018-06-19 \303\240 18.18.32.png" new file mode 100644 index 0000000000000000000000000000000000000000..963a9293ae1d940adcd2134b65a095392119cc60 GIT binary patch literal 176862 zcmeAS@N?(olHy`uVBq!ia0y~y;P}D7!1|7Zje&vT&r+6r1_mDWOlRi+PiJR^fTH}g z%$!sP29M6E)7c|}Pl`1>pB5q%rN9*Al;0%s@>2E^FVUP9l?ee!9voU_DS?e!7c6QL zjTPOvmUY6>w!Jz{d+kLW1Fs!z;+<;l$q@VZUiE$J`{v*OSr$J#WBL5t+&hd8x^2p0 z>5E@6Jn8tPvT-5f8qSlNI+v0gn6(s`iyfX6EKT}9V+IF9>WhE=mF2<;pMINK?_59s z_y5%yAASVsF?jGiF_|}IHuFybh8^1BIwzSME--&xcKP#>37rOB3sf5!oy;Z|w@teH zbk4FT9Id`JISNlg7*3e*xldwjSY@LZW@pa)D^R!jjlv^N*9o%~eiyLH$UQkf!TsqI zpXN1btT(mZTH8zh|1i_+&E&%;CDKy$xD?qrmp;llcjo_Qng7v6n^K*NXB=`9_BfRE z)bjM}X{Nd{=S)sKcg+mEIKfmgUi_2Dx1x7KCk_d*y|ph`+PERDfs?`OabeRT#n;Ye zf~oGVJg;gmwJPSE*zhf5k3qledYSG|DwmVk|1LKEap}Ph#b+KO#V2#UQX)D3HQSkU z{dl7|zn8V?hf3<4jO={|hFqJpp8USFX2%D|+0R*<4y;g=?$imBGp>0i;W54Ch*qw_ zW|@}(aWj@XoamE!(qZFRGowhyto3x@k!8%5llA8c7>cO(%Zscz!>#?KTZrxP@l!`D z88<e6yM5%QVe_{LVSDS4b>@wCl+N}`OWmH$E<W2RUZ&%7(xa&xA4bibQ)qZfqCi@J zk;zdYfl)FiGePPE%b{uWe#!mtzA4OP;lO0XAb*25tIbVCv&!Xz6g$^}_z6E8tZeig zE}mxp8Sp<fV)dR|jU}%6`;uJ5UNp^NKICZjM>n`fi1++7j(HBfAC}+TX>cv)#*<q6 zml0XcR!Ihi9TILlicycNcJRMzbw99LKF4o|^*ck6>*m!?;tP8}%qVES;J3p#G(zL2 zVv$$}%UgqE6Ar$)wC_CM_J+9F4-9vtf3@!`R9naX=7V8@TKygIb8nfp-aoD-eeOrJ zE_-dDjD|)k%hCR%ugNT{%vR|gj(alo*FLdRJ1(`Y7PYwZIa#jp{?&f=_Ydu(lDaoG z->`h6QpT8l=oKTkb)RX%^o@&)<l?0LMY5AKwnezwL|*9su>7Y*bMdLwYNabmUzGi@ zxxxPFhw-N;`rZ#rKKgVTI8QkKseQNU{G(!pZ{0V}k&$_*r*;4L)acXF%aZ)vXNd2A zS>(^~AdqRQ<oyzv9qh6%nHoO6n|<z!Y0S6Nj25>}P4)aV)heb=@xXr((;v%TFgfoi zk385jr$PGzQ=NiD5A#I@IhAJ34d<A4_^BpX&EOSlNDAPZ)_iq=P!3b{LB9>`G7MiA zaL!>#Kd5?vE3cutU~Ng0_yX}g%zrtWCJ0y^awrj0J|eTxb&f#!5q3^Dr3n!#lAf+c z0)k4;Y0BIagO<pwblIk0dgA>F!6zz3jG4~w6#6IRJu&^nc{D(3Rjbqju@@OvWWTx} zzF1LWG^_F1BGwGST_UmFVT*TPwECh_#ksd}+hY9-c037M62ivE-WZhY2y`C{+EC)t zboQv%2Cg+yx4TUbsn_ymAFtZbZy}ieK<5tMy6XlujL#qK{1LN<XZ?}#N2Nb9|49Ag zcz<-i&<mE@Cf$RR52*PlqzEh%?&1_}dE0Wh#rVVBmnwUdg*tr}sb1vWSAFt`&m)~A z$wKBr`Hw6cdv7?tQJ(0rOC@@ex#p}RiccqYdA|18?RnfY+;i>{{uI$of;T&5oct8K zPqaSa`lR>C@Kflg;39k1xk-K>m$(F1Oqn?)bxQ6Ow^O2C>PzRZa@@M4D){lrnO9h^ zY`wDgit&}-SIo7vwA8fZv>u1HXnhX7w(3o|<@!$^PnCTqr#W*k@m+K>!#mSDLp`HB zBmYIoF4nvK^PJ8v)?a%4g8xPNOZF>_Lf(enTxD~$a#iT6(pBN1@v8!_oVohv>arE0 zD?(SuhU{K+ZLweA_5l8n=PS;Kt`D+bROh|dzj@)q6@qIgE)HC_(fe`JF=O9J%UaVm zsQGX9C|><c^DH0RF@sbVqa8i1J-I!`J$|~2rY#kV)(&1BlHDKH9kzbkvTqV^UEWIG z3ccmN_4Z7O%S|WK(u}7Y@*CtE+4mYqJhX4JPdeGLS9x7u#d_nQ<NodIgBNY~f8KUZ zJ$72&hP-L-xb9ZnWv|*XulHWX-j`L<U(dbx+H*zfrRgN=O({<rCzgK=td;&7@i+2U z^jH1Y-z6oaTBJI7uSw1^dSRF%^-D5MYMqfy+Kt>J2A>Rtq}Cc)<$am6^z5hf)Xmp6 z`E9Q1{n~rDC-qo#Pjt_3Umaf|A1>c-Gi?l(&e}D5+3a;jxn^%ojg7iZX6J@wmtCKe z^e#`&AU?V7+@HOzOI;@&RqC$lPM>BqE$1}T>8R7c!%DQ{SI4Z*TK)Q(OLqBdo!7kA z)|##8?e0CW?9{U^-K^JH+FR}>+&1enIF&plBjoFf=rR-Q`rS3VqjtN0^ZCv3+v#`J z@9B%rx-IXCGyHYu#d<zHzMFivW!_n4S@jj`l;o7uyiIw0q;yr$u6I7=e1-F#-+AyS ztLLr6zZt#BdAki_lw!qpn(VGAEG?e8<6Y_7GUH<J(&+bPr(;js9@~57?;iGp$_J9? zxlcas?jL^M<~;Mf`s$mNsg-X(RheBY(y4#5ZKLcxwR<w}b??2k)0rPK|E%5HeaH66 z+|Rmyylz>Y&HlG_{(sm0HU3-vdp}PB#{?D`z6$vlk{0S8Tm-~Kq(X$Q2(6KvBM>9` z$KZ&Hit-Y(CFWODOTu1s8aPE%-00d6ox^N4J@)WB+2>|?=XP9vxaGr+52rpH7c>)` z)N#ziq$uXesT02gp9L;j@hd~%Y+sz@Uxi~vYUa=SKOXsL_;~fP@}pMA&h^=fAMcng zwO#vNY>mU8O+TtR!#Fp!JagH4#Iw`1^Zn$eNv9^?Qtz2(kbY=s%HEX3B4;j5Rm0S3 zulq+Hbgot}^DCMeV)OD@%ITBFYVk7`nX6Wxy0|hc*fN-ZnNZN=pz@`4&s>UES=MI0 z+441W?;VGCQt!Cu$>)o%pZ)%<{&RcZ?|%CJ_0t~C-DtEp`hoTefiE1)ty2$$&rF;- z+vt9}Yx>(0QYZXQ-J9-w&hxCL(j}49PCtL+IcCXGvu}jO=;WRMdM0-6ZEeo!In$Lc zoxId$qx)lGZDgA2Z`W@c_tp1n{XhKW$V>M0j57jfci3D`dp2pNKerQC-R#!$kIrSy z%eARK_{P2X&-0v_-DiAf-?lkl5_fUWxt|L^m+LLiI}!CEeW7z=<-^;pp4`&iJYCPk zOaBP{^3L6uwQ<RhYf9@~XNx`VZtaeqW}zRs{Yf_G`X@;%oikluJ~zs{c{E<OKKA$P z1DC!m)jb_EJvYWLMlY(axb=P4+^14cS)a;kD~DfOo3vId@@v`GD_6y}j)mkzImaqT zU*2bylzZ4}bLr{pwd+Je?ycZjacg^A{ohGn-<y9hi@Ez^d*OSvX5Xc=?Q+HP_U%<I z@s8c<T6#3>(X~HoV{Z#>pZojl-|e&BR^9gAUVl^Z*2R6sWzSzqZJ#wO_Sf!dR~K#F zbnD&S$z|K$ao)PT;d<WtX|-o+m;OFjck|!#{mcK=eRlZK_^kOI`*%4b`z`+r9v67Z z2AOYOw>>W4{=ErzE|jI*nfT0Y{jtOKH~w#2d~(OiS1Z0<Og+Bhcvl~<jK0Oaa;KU_ z&r^<_?0dHI>~}w3i_*Veeq7c~KXiVMy>!vHE0+I@kDsrepTFxt<--%M-@Cs~kI}yx zTeh?2hyQeY{_Q)qU5PE+ed)rhi??o=ZF_qo^!{q~^{lbh)pKkAUg}*Toog<fcW<BR z%dp?M_ui(LZ+gFH*X;W3|L^_z-SS(N|B8Hw&6WCyzxuE1-)5KR+W*pcaq;Q#!v6QA z1=evk6YYN9iM-<aepN-*mo3j`Uf#Uy-Gq17o(s<}-zTvD#e2<v-jCOR)n6MY_v`li z_ow#?S6Rn<#_{gat6uy&?f7lO{Mo;ge#z}SU3vZgz9-G^+ULsOwV(HU%9od!|G%7H zaz1;1L_Mg&`})t}+<}G}$`3W{SPgDm_)@|U_nx6)9?yQ+D8>bTA@k0PG-;fl%*FJ- zbLw;R)*Vb64)Zs({A6%BkUyn&!%XL82Y3Gwwz<c@;M}>xxdBUFp7ZmHH>pTctdWqY zNM>MIDcGPtsj=}NFEexewu1@p848rbE7hiS|KZtnGtO`A-2UE!{Gakaye~A<S#kV5 z0|NtRfk$L91A~|<2s3&HseE8yV4R;B5>XQ2>tmIipR1RclAn~SSCL!500K4@Ru#Dg zxv3?I3Kh9IdBs*0wn|_XR(Zu%AYpwa1+bEmY+I!W-v9;Y{GwC^Q#}(s10_2y1qB70 zqLehNAQv~N_M((DTcwPWk^(Dz{qpj1y>er{{GxPyLrY6beFGzXBO~3Slr-Jq%Dj@q z3f;V7Wr!g#b6ir3lZ!G7N;32F6hP)CCgqow*eWT3EK-00h&xj&G7&cA6+^w1oS&-? zlF>KRGth^d4Kf}iY88-Kk(v|Xl9`*DSDcxjXJ=$&X=Ve~fFXsj(gvY55~0=5)X)N1 zE0PpctBpR$3rJpqgcn#e$i>Z$%SIm@vY=45;}TYou3%tbkjZrM4`E<nU;;%B0|V1{ z1_lNV2+j1JfiYr<@->|=3=9qoo-U3d6^w80me06y<=26a(>HBdI(@14`_L-EB_8@* zOqX;TZd{mOwYTj3*0}2Y6&*>A0<3}))MQy+UUIbJ(A!%sR%f|6t!$IA^~p1SWk$yL zZTNkPii(QPJ@@@Kf6sZ#nd`D<Z%}MtVB%09hG<Y`HJbR~@rfN5G+6{37#N8mVwhBZ zd{7|Dc#tz5Brpoq$jHd-Y6J0zaTEimoeeW_?q^`)u!v}2k!NRbUq`gB9h&&h6YUlz zJ~4%Z#}6J{m`${cCOkN7Oq4%Ck<`%4>hYnlpx}iRh)0yORDOKm84Z4%AwcWkSM>fM zxihV`=iUZ4GdU{-&+zq1-3@P<B8^ffy>jh3)gV=x;xU(P+kO2B(lvXt*7rp3_BWHY z(G9PVXIj{OW7o{B&o@Rn<-H8vz2r{V`Vxo79WDFkzpT8k^sv^${|^&anZQ)#gZdqb zu1{27R{AL^_Vws1$$MM%;z=5YN&=Hu<|i7fo!sI3I;o{6(JF9(|JJBaf$i`5UffdN zzh*k;>;Gjg@h@B|SgNk+=;fEL%zXK5;*amf6Ta&T-EVhzl&*Q=<qDC`x|b4pF0oQ4 ziiJ80Q)&diT3m|6QzCRQHkPRWSTR$5-VAGHwiObKF0;nv-c@{K{Bwn*)V#l?o1dJT z<{I}pBGxKQQ!COqy~s-TFL&dCr`qAEzos#N`thZ==v&exoBQilz4Wo0_~-lOQ@@w( z=U04i_Ji7*Bj?}wy{!9VyD|D!)$fCQo>$4Ny?@lwbnPDYi*|O;U7F|NjDCd%hc(<w ztPd_fTdtgX_u!F;@{~V(0xPBl_&oa}FLZ5QiR{<iITi(8b}!<*cZF$-UE5di`2&Z~ z%_$oEK2hqXUC$<JmYaOM-Rbe~cjnXD%lFy$bMV<_s@9d2$9{kOzVq&-#_jSas@EMi z{II*_KbQ7<$1VN(e*zb<<0~E<HaYAPuIbwQeUnP;-Y#{fMDY`oG$*Ur{RoNp9r*BT zWRYvN{5HEPX5;TIra}*Q@0|5;@g?caDx1IA<ki1n+b`SsWdHoId***@KXgC1Fgff| zdinKI`+xIp=*m|7ZUhAm^M3Ci>o<IobNT(_kUVP@-qJ?}96Bd<|356mc$`C~>cz_q z-<ICV-oNYJ{?8xF65q0$^RI8X`ds1C|GHUw^LIH{EEfE8_wZl2*L4ryxqoQ7SiAJ< zj~8E+LJz<B+5PRKYu23?=6H%Om-!5wb{EAj&ybrKd8qNO#jke1&HRhXzBhk6tex=H zQsJQbo<gJD)jRGduRcHVriDd*e*?()H!oZNuAQ|P6x+XBcQWI#Kq0}mfki&s?@E^d zqfm{KN<iQ}-@nb<Z?+c-)y(;;FyrX?clV-e``)}`fBRc(gOvQ<dGEvK#Akg2h1DE+ z#i|ZG8Ed1GLzDj+7b~}8o=yHeEVmvWzG7c7yH-N|;PIP{Cf)Pz?%n=N=H^}gTi<mP zOzo;{@2|Dkf8~vB$G4A`_D){}@KmQst_>{nBRWfZdM2~Xm*CuTxP4{)2eaP>eog(G znKQ-gcUR|swcNB@e#`sl16eg+s`f<})~&o*E%5enab3#`MLgkgq>F*mZllmk2}yNM zJ42z2cK+c14|2aH&S8;HXTPLVSN8qhtKyV$`;7bB!699<{n4MG&EFMn9e@6Z<DxUZ z{4UBURFkB7$<WA8s3u9}0-yY{e+O^BIc&jccaCpK<exY1_FjE{;$7W^eff;5KVE$O zGw<lnrD>oHa{i~_qINtPL_nKK<;M}PCCMpsRDK-sSRiZX1CK&kkIg@B-mPAJe&XG~ z3u^B(g*{$;{qx<f&)#S6HD}1r|E%o7FNnM4q7cU7@u737$MF*uo*z0VI#}1t_{E;T zng6lM4-4xFX&*Q5{vI}8dG~L}-}_kC9CKg)e7ETH>5z2y+`U6i33pC%Si>>lfw1<( zzKMmC9tbNpmi;jJ%6~6S{+Q>7!ZL-kkJ8KEueEarCG=YEh~ECV^X1y-^i8Yz7uD9B z>ld(f!QI$uj1W{fs2-*ytNd8ypt>8=+YgDa<oBGl>zVxE@f(L(qX~LQBxu384}x{< z`nPDrx=Bsd(B=I2F-Rj_@8oRT_Bp!>9Q)>2YqOu5Gi~Yb@23v!-5v9LAy?M5Gc%Vi zDAV7){quDFlb*Rcjd`ubN4?*+B(1Br+?+PGl%4;?tmgXD_a>_|<!U|n<+`Oh{nV6` z1$Py7`NDZszsXK`kUU>kH(LAxtc==~X^{m=E_RV*mwy6<oOY30o=lv*DrEZa*XC1a zxpqbMUEcJ!{-pKky2+8dw?*z#TifO9HEr6-z4_j+OHQ0Ro)@Fde`Ze3&b@x3TBo(w zT6vtbsjQ7Fe)Gq&vixWMr;n$W+MQp%eM5b()u*`K>)O)P^6#oN*UwDQ*!;PHMc%qJ z^Y*F+Sh*7BdN}wzsLVP=cA3@K;q@UicG;di7ne<*a&o%;iyv!z?x>gh&ef>)ljGmj zn>TgUN%7b!w%WZ{x!0=wSRvUuFYjKIcU4x>%9rumawq=(U0M{dnR#g`v*_fX%<Wz^ zF=pkWrp-a8^X(=tj!Fu2%T@W2GR5TkNA(4;65Rgihxx|c^Y7YjA-i@^P!g&MVLJWT zcl~sBX*1!+kNgj|zC6<Ux#<2EPov`J*A_@g1TAmeb8DA#z;(8wiBnnRpMHFNmU&6) z%T0|Yy4L;jlfNbJoA7v+Yn8d_>PsrQZj0Siy(DM7vMyY6=HAx30cqbEIqk~0FYgm& zgq5^wGTPSnflBe2<d@=o{vQHQDl6~4qZ6wY#+m7(7r#@9S-qEAi}5L^?#&kyk6mxw zV!uyn^P~rtouk*i+3aV_GkyNPia@)lpSyK_Wlx{8x5RVSv^{QZ*K%K&e!loyd)kTa z?q?F~c%MC3=yvw$%e1vT)9ZGYEwwRA-?dOjna|_HNsiQaiE8kmpP5o|em1CtBtOYN z_`x!Nd;SZZ?br60ovp~REAkL~b5k?APfl#>{^v(HHRqbI57CQoStX{WW1h)raP-mV zAA90d_m}*Am>pSeaPj%iEnN4rZb#;=PJJ`kYn5gB&!DL3ETMKE&-Z@1vS(&y-0C#5 zsFK%rgBChCmd_0q*FT|a-#hVX{rlqSE9XAi`T4|=IOe4_|EBC(wV!w2p9sDz?@ZR+ zSN$~9F8}76WU*HN*S5bm0yAxYeV$PL;_dI>st1qH_scjJyQ9YS+dK>P&gJJ$Kin+7 z)S)jYNXNfSbFTgJCtat7X3fr6`^LU9t|;rR=m|;jO=iZc6#qgSNk&>q2k#!&sXMqe z>#x@8^Ao>{t-bn0cKfpwP>Uvet=0NzkakC$%L_q#?T%Fqi-l|Q)`$Av%ej3rJ;yI@ zhr)Wjnfmc-A{9Nu*Z*3xnCo-edA;*-*TeFz3KiApzTf)r0%PXqyB<3qE$_Y3TYq=$ z_L5ZrHv|40ICFAQ+MSapavgeieahRd*B1S+`kTqmth-vyGwTAjXB7V2dOzXsg|EMN zKKuUUj%1tBYIe&_X;W|hZO=Fv(t3CLikTC4_5Ah<Q+-!HJ@tv-7PTLHOrHBkZh7tD zp&>o(l)nD9^<o9lr@zTfmcRCPdS*1=shQo=_pUoX&vf&awGl3P>mr?Qg?Ln|?a}(B zIb-|d4K@2}w&&Osc=gTo_M4$S(Y*bN-n4MJ-TNj@(VlVo{MBd6kFCG2uRQH-S$NH+ z(A1s#Y81a0t|<EgZ47Wu-C_H7#?kZhb}!qK_0`gAr&4Y8p2v+pBYXSf^fz3a8+;qm zU{n|S!h)~C7<*tNr`^xZ&nGM`zoX4sTvdNz{@(1}f9|gjW|~#GEcYhYr@Ot|J<@Vd zuD^0ydGqp9*I$P%*Yo>aaZ9({qxq34@AKaoo1dMzyZ3NF+V_Jkv)MhmFQwky9%w9d zRpR5i6*pTh-Fn8iBRES;^Oargn?IH-|D3%2?y*BzT;A1tTfa<yZkqpRUqIad+Q#(2 zws-E?P5$#QYNz~+Kkk%tb5o4s&sF}mr*l`P%zK`eJ5he?6S>v1y}neg4KCH3qCI=5 zcSzG+>+Z4>?%m}cQ7@}L-rRKRb=2&+tC!jApC<Kx`K5P{pD#H*-FwTEB`V>aUQSXs zPg@9wE4=-=dNsBZb?J8bJ&zA^MuEa_$L^N@THwa3_&<&d%y=5F4hoT=0Q|pqU9ZiB z1uMhnl*!pIJG<la&ig82>%0GHR;+W4mJHk9c|CLMmhkR;yU$wws}r6+NG$Tb_owZ$ z?$+1lnQv2`Y|Hxa_f}EL&pX>?_E$FFF|J+o`^NP?sr^MC?r8DU2Vd#&Xv|r=PE-AH z)i?7qT}#s525$boBC_VtR@dw%|Jj23Po7(x?tDnfqv?F(<y({T7f<i@>f<a4K53i$ z%XETQ>tkR2+V|UKKfPP@_FWb0)@<qNdJFbGpPu?O{`k{3OP?%}34M5b-`Ay;Gp8(l zc1Qf@&A`nycNeL8uDw%!C)8W_w`sD6v8GVIF5^l6%Xa=RVQt3A=M7syE#1Xo^A~^l z(p+v+VGC-Wv(_*F5SIz=2OU~{{w|(IG()2Z<8iegYvevPOgS~#eChIA_l_=6dHLsW z_@-wM?za`KX1}>rb?LKj`-8Pazr;;ielq-U>hFz<r_H)m{6Be;arT^r>t?aKpKY5Q z7#;KM%%<;+>MGsGdtCDMVl~$5ezdRlo8A?g5^_DREpL_TscW)6CR2V|YfMmHen#FR zr0e;)$Lq@8eNxS|{Z)Lr?H#xC9^ti|c3bby)n2ZrqTjtVX`XfUwFOUSWu7#YzaMh@ z%dgG1e9ZT5eKN0?-Cy!m+SxgtR$-d2qRg}v%gZc_zAehR^a|A3f37b*dAHZo)0*~o zZ+US2_FNL^UAXm6$;3JT(rb9Ps2*|qx-aZXk8RfN(l=|qNN)}j@}FBSdtG<p`K!xb zA9z$RuXj^3U-!ndb$>nktu8GrYWi()#TwSLXo<4h@^<D?{VnC6^Ul61ep0)1tI)l| zY`@H_o22CT#znog*n0<(>22-4@Z-z$?gu7v+HDo?I{D_G^PWW?-fg=7q`v!k<y7{d z4+)3UKKXpU7B~G5v~XPi=?d$|!ka1AH@Nd2)%v*N3Hy4fQ*Z1vX0LD1kuRD5(5(O5 zOtr<=e#tjSMfvC&KHJGV%P4MH&h0mmPs;0Ep34;lKYi6^)qj4jUi_NKObzbijwWh9 zVlGTdTeZ*h_Suy}33U<|F4X2+KUXV!{nQp+cAm55`q2URUIhhrWp&!F-2V8v{>l2Z zyfW2a74w4s$}azUy}EGElc@FQZfnigyZh8C;H2|@{_K}qw_LyTwmtPjzvk%|wROd@ zfu|cHZ(dWKH)Y;^oy%#{yyE_>4qq$u$?dsRm>%!0K3Gf8Q8@hsI28+r&0n1G`=GVl znwU?epw6K8{kw<Cw}R^^?)f(5Zn!&Ej4VA&eLf#{ZuVoYTw2PW?7ww!obbNMKdZ~C zrm_cJ*p|a8Ssfg|-h0iNy2W~*rbcZ3{)BVo_oMA`xwj$<_HJ{z)_+SVa^CH?lk2Cf zyuH0btn%$@)xB3P#m&22%$<JPUAz9fj#u&i@T;?o_B`%w`@Fe4cltiz<5H&%l?wE7 z+Wq#QH~raR@m-$QnVX+S%xpYf>nLEo|Gnk1v$HlIDoCBa%Ju2pysa*(50`9RDKhJM zdhWFKVed>sD)(3Gz6@JCW1)}zi+@*x0zECVyt8w{x2SHL#UKA<-qG}e=Ikf-xvO9J zF8R4SP2`o?tMZ#=PkL7O%?ix8wLtCVEJn?<vn%JoI|a;V8+U_BgVUgJ{B_V;u4mRq zDNuPJxxR0XGNdfEoVaK?zV7;4P%u`miqx*$5m>DMJ3yy7)WmN48u?!f!f$7NlQZq^ zzj<v#h33qv({J0dPXGURdTYsyg%_SN#~XjVWBOM8XXa_Q{pDe&C+qHeE9We$ZTx=) z_mxSRpSLXC`6>1M!+rl&Y}1JZwXd1e{dS)|v{Sg3(=PkJou+x+-YaLT-2Y}jxhZ{S za`YW>mK}>HmF((|(t6FWqJDS9>C;cnYwpkJH2k$G^53iw<EJYmi@z+94GmYlc1G@& z&!*IqZ%SWoeScQi^mqTQd0DZMZDv}JL)}I1*e3R#xl?tabY1LHrB9LPciFwJ6tjCi zF=t9-^sOA9{l6})C}BLWwe8JoXpx%CU4KH#v^)Mp-V`_Iu=$Hk^!zu^D7W#Q)!+x} z>gz8!CL3}SlEXDbN?7sb@EA}IU%w?wzjoDCvzD~`D}^t8x-HnYDS<Df=+w4q`H#D< zyyCHo)e4&!7+Wp5ZvLLq$vpp3n--pV*Q~VjQR?GZe#yMg^I11PnH;Lwsn9n0zl@1~ zsGHh1sSJy&&zBn-m4D}Rj(o*zntr-vHv6g4rM)M`v+iX5@w>Um@L>A6Z_Y+fj@#ck z(jOo1`7Sf)TCL10*?8mXnv&$)mut>GNG^@u^?k;dzvnENFSJKVXNf7Ce|x3%bkOg+ zJKrrT-*t7$>{a6B^40HbjEg7t#QmMVdWF}-J4Uqy!RIf>{pK#0n|S;9TA`_D7Hr!s zJ$<TrRH&EThM)DYc4rQsi`8uN?s%)&=6mnFDo(i-u!?KX<CP9gHd{cY<b>^<zf-}Z zNBVCV7vP-_I$#e<j~k7xU?pW<;rlQ1pB_K^dhe-V@jHRL=YcwZ3oDMupMJWs(wH|@ zvT#H7jpRw%dgp3a^M~n76c;;NDF6G`%fO|(YObwpe;D#O@Nnw!kBi$jU19%zEn7!_ z%btS)HhGt=B+Zg1E!4TgesRWw&wp>#emb&c%FLPf-bDphUHLGVMVRaV^7NcZ@BexD zEeU-4^z6LQY4!zs&)+P+v}?ZCs?3GD+S~l2<9zO0h4@YIuex%PKX!k0-~7q1?PH(l zN#9((Zp+GmBeidKe!lf*?WdFPcgMJL?%aI(QlhS{zx21OJr1(&dJ~zK>+d+VTJ_1q z`T5x@g?4EIMllkLm|;Qc#F7_moc3|^%Wdz{*4nwh<JNiip)b0{Xk{9xEkF60O!X~D zsx&hw;lY<G{S=-F)#R;;iFGS6Q`@m(#di76OV`+Kxc`N*Vv8qt<(f{zjSpOPS3axN z-Kn;CJL~p)s#m#|syOT1+8tfh^Z#1DdB~(buWJ{#YlpA*ddVL@x!cpTYU1_$t%o>` zR^Lz1J*eFM`_8?|>PxP^5G?xSRK(Q!+hs=iBEy5v7pq!7J<EFg&Zhg{*6N=<Em9)i zUgWO3{_2avKa1YPe3}%w>g~T$@00(nLb!jJ<!9FZ3wXVIU+Vc;kzrY#PV-LJ#QN+~ z3cWw)?Yz|IU(cB?DPJq}OSSF(@vSP$3!^qozV%Avyt?q#mbqKD{jQJG-k)Rlz-sL@ z*4Z`H$t%~K{WUpoHmtgl6rK3sU9#Bz2T@z=re1x1qN-bI&*R37pX;+hA^Cr;<@Yt< z8mBlf<%J}^UePRvGpavouCAS4vM%7Lz;om2dS1Qf?+Jg+-nRGNvc){BV($mMt;-dg z{eJ7soS=Ny$Hm@<A1z%n<J;<Qk2j}IR(^i&)Wgl`sq42rx$*w@w-YzLH9JLCt~a+1 zGyb?jvhs`X*;$u9obFHg-ubv{;zM)2;<U)>=l?wub1U_0Z&!WkF!}vU`0s_+;i_NH zys`Gk67h_T@>FI0Dq|m2_d9!g_}>C?m*l4NpEqwiv0`fVoh`=gCX>BdgOgs^2{z6B z%I@d2J@8oSy<_}2Dqp>xaqsvl>wezjwqQ5!_1fz0X>&rZ&6soL%j4Eb6<U?c=DSCQ zP5C{0nsLlxd7fX1ThH7syis~%ZImIX{`x!RVfmxB{ZAiiYHlvHy}D}?Pj1MufaCAr z3HME(%dcDJ-SNMo%=h1VRh)7!$cy*hN0II?@4(%pz2CX7e}$9<G4Jk(<4L&!9=Za- zoOVk;Kb!otxy^H$&rBY^`D^3U{LlZt8ZhnJ!RfL2)7FWumh$^P>%-hL_b1x~d!AcU zXtcu5_GQ?{Sy%Mqf_lo0VxG-1eg6G#b*y)D@9iC7leQe0>9Ea;^Jr?=|ATGbcGstI zR~z*{J(RaH%-inGg}VX&55_M)Q7(N=;*<^lzv@`&$_0+W=aqg|JPoQWd#K|)GnfyQ zv>QENr<_{mV*YVfd(`e}XN$x$th2)Ul+*Q3z2u(0wB+9$M&0F4Z=UOa{q6MPVDlZ_ zT~ANydA~D07xC0CGjaK0aqlPRotNJ=tyR5!=hI!T$Zg+etoCaT`uVh-k@xP`uX8s~ zpR{FWtJsN|-P2=T4{uRfP@jF$VdeI}w<aaXZmphoN#@G6FSDAAcE#83f1miu%&Bk< zv~%UJvE^;{VV$~fcP)3Ou09{A@b`f%zsu?RSWu(jXa3op>5!53VrA#^0(eH+udyU) z9ejSbEH=fVcUPOl>LpU9uY044PM!MZJ^%L7z|NqyRo83(uFSElim(b@q4R(DzP&44 z7CCHxd%W+|>F(zzm#m%5w5ufKdfcU@iwfNq{r_tgelIp}j^)zWon4DwRq1Ws`uIm+ z=C`ZrnQCID+NHgJN<KTBd^1sC>&Fj{ndZ;9_j^~dnluQ6_Z?%IUzZ$q;(hfb;e?`$ z(=oYKOV)b(>Ms8JcUG-Y@oUG(&)+=*jkQ<mW<T=@T9=@=GD@XC#P@{m_A7dttwpKD zbB%w$KYBCzNw=uxX4a)s3zDz()zzKouRk&8ZTlDJmyw%y?76yh+3$$AJwG4oS7y1L zDz+~T5MDWL!tsFgFR%7LnK|=h>6OnGzN;r!ozyk`$Cw<xYwf3$^IDhL&8G@g7E9bZ z)d5fVMoLl#?;qBwJGedPt=H=F6SrQQl>M=febLkX>p;_Bf3`onvl*Q5&+nYLFdbi; zESt%2@`KB3gG_%e^7dc<!|zY=VxGCbCWlu>v7Uan!~fJ%{RE5H!XME^@qdqfTJLsU z@>H|skF2{|l|}D(c9uNtT6%i5sFu=aHa=68hby?R%lVY(E?hc|Av@}mRCrKOSZ=4@ z`M2|43a9>gHsxlN&H1M%r^P<Yep{uc9qZ#&r22h@iv04ti!&Zve)sw4rne^~#VaLq z%jQ*<$3OS#s?7aoW?6WzrgJ-!<>MzirB``<e|6O7@Uxvf(^lPC=@t0%$Ff_?t}hLJ z*!faA*3;wH&us4}lR3AWUE+`3pY?6wHs!j>w#U!!?CXAZPH+DARobTA&q`J2pZ)WB zfsIM!-f1Dz{`jpZ+;jbGb>%w2(`Bbm7ymEW{<rM<tV@gC3cnY+y?X&|woRTG$oVHO z?IZV|qX*V)_$9-7N$Y)uLe#(4QK0#m`s{PI+2HQ*9BIiae5+M9stE{l+G%;F-fLKR zYm?he-jBAm9~Z^^f6H;R_|uMsckUlN_gil2otr^v&X)B*H)TJYD;{<4!TU`O=XC#N z_w&s7vUkP5$eV3fG8OdW?)yCoQvL8__qWfc+D|fbHT7$+O}|olH7G92Q~&(ipRJpp z9-g|tTFf%fUP`!TU2Xn6ng8k&mnW`dy0j_Au`>JNw}!X3!@VX<w+fl8^ilJC{wue< ze`Pb*+?sAQ`)cl2uhXC3_IO`fy6@QikGG@j?IZRaj$N9xu;S;}b7}9AYtwH{V=nrC z{x#3}<fdsa7EgN?bh_{5>{(OOFTL$un$s9lHSu+M_cor0@9U@Sn%rO&uB#<~SE43q z(!!4ld|zPWTTJH6`i9-}@5XKfg(&~Mj}BY@T;B?sX#Ia}PW3fNgTvhD3*H8Yufi+g znxeGc$@8oEr(}0e`_T3F@aDRS`PctH44oI&n3aA0Z@;A9YS)QIl3tVeuY3yM|L%@j z{ah=}`uTR6T7oOQ4ju|xThJ78{a>g~z=;hpVXxGdz5fxMH~UMoivF%Czt();7-g2T z#LPKxW5wf3ZY#6r?XL0sXPUk0&yLF4SHbU+<2TE$T(Vg1@qSa4gdO??7yP!U{#bD{ zS8KoF>Ax~7Sf&Tdaq8dPs=9mDbWN4(8OFawPR{k^o3l$n>!$19%afaI4hL`b$eUT? zE_~f6Zr;;hb?p<qpPWCu{HycN^3z>LYcegr7cQ{AzviCy|G7JF)<$l$xtV&hR`>4l zdDh)!vJ?N$-r{%VQu02ttKa>ru1qvqIjQ=}89Vc=leV+B8m{&|%V1LL5qxsK^0{zh z$)IJhg1fCn`NixRNA+K{>BePzwcK=L&T1)g6ZBP1yDZOD`MbVOlRg#trR(($-`6QO zdOYKv{^Pph<FK0T{`>V^J~uWw+Wk-7sq?>j{{1S|qUTe$-zxib)BNwD?bCH9g^QiP z&hJ~_{iJJpyOi1F?Cz-(q*}YZ*6n!0!oKy+^|ai`wOiYTo?gCsI(71V)ooqJo=*cc z99fH>l-z%R<tX>MnSI~p`OLg|wDwY>^~GrqCa+L^WAeA6ae7hBHl1}(X3sPA&M*#6 z{c5#7Elp}?P5+*~1+%Bcq{X~@yYRKviRQDXt78{)&Ek*O`0YQhe17%bV&0drr>!l| z#U4ya-?eSewY~Oh3$J&bpB8$}{?4u1IaTg=tN;A^yGifkk~Q`PUXLDc+A;0bs!*Ht zUseYP6=fY?xjp<@?$q<s-W7@0UtK=W>~x^w_d*NAu&#T_n){b^>aIm|ty^~WxyFQR zmB*Pceu}>inyvpAUHmQ*+?9TQ=fnkLJj0S5rj09nKLl=mwdel6*<aSa+v{`wR&(d2 z$~)mjj<ZA6C#<d8ILT=5ds#dEz|)zwM%Amd4<1i%HQ~By6rsO0OaJHQ=O0#0T%ww} z<;Sz&n^j*ru3K(yIy2MAQ#E(m0-0NDoM-pRT$!flWwiT<sqn=cvwwNb-XzLXDnG+A z@>pI>w#Mhu1@Bg-sjXUbHe|^(+uJoyCM^B^D@*&vrVQ^7k@XLjlt^#Aa$w=Koxj$k z$7+UO*x@sE;U#<a-F<VX?%v8W+y4K|yKbWCXXkh>%h>!Q<YL+p&%G;Cl%|F($<Hrc zscD-ncAcO3-`uTlC-BJ5H}an5<rDS4V0Oc8&(}>$tNT6w8*1M*_FlSm$&9M$Vs9*z zq)q&%S}c+EI-69v#O>mqbtwv|LZ^J8ix=hxtZLt<yZJ}!zrJH)Ywg^Rt^OnbvG4bH z&L?X?6(v*VZO>9jJ7sc`W4r*KcFH!^MP>(|@4vovBD<8y*RH%rGlMS`zR|e3Tz9VM zi)$NeQhzS1&R>7}3Xi$$u{y~RrM|2+G5?s1@(%=j@3Ed|{^RF)-=gPNIOn-;d&|Er zI6f}>?!^oLrsU2}y)flto6&jZsbYoYdyie(8x(qYjpM4mS(X{y)1DaDpZBXQJy~<z zD0SZTTN~wbIqg1|$xL&!UsD$R^jg~MnMKvf>r+?V5v_T%;_0qkeZOL_6vh9#wjiYL zXQG4i-wS_lL+jP=ABERyoxIz7dRIU5UCG^P^KC3^7ysVGlKXSb(!(|S7oV<6DRkEj zz4o$Ob<_L#lI^F&d#(#jm#RMdsrbbi*PZ{LZ{6hM&Gu=lW;cKSnzvE6PGA3-a0*_Q zyO}>;X9$|Pq^vC060Ui-`hLW`{KDpNolqse>xJ>B-R!+XDr27n7G$5ze)?A0s(Omb z`&BFwQ#mHj%gwmk_Gy#ZWdB#bOH)iFO=HSr>}UN7@#w#L#=4k|XX2b&J@&uK9-PU{ z{P~u{vg+NMxRf8EoB3+5Y}~y?Km26CyVnVa7cv{YnEr9o=N<1=xZ|(01dHWz+C}rv z7c}MW+jmuaroYs5y#?}7|KP*xzaqEXoM!fCb#Kb~SvO;sl~vVj_W5^X{w9&z6WUUr z%ru+ITgqhlBjF=o?#vBSey>uQbJKY?>m`?2rT<M+_x{=PF#md`Df9UsjlD~@ZJRRv z`L@R|Bwzl%%N6Kp_ivli&P^w8{Nr^w+b7b6NWU4kueU}1d9%B?2sHHO@av$p+^q7L znbDwmj0qN&E7w6fltDT#xbbx;w>ex<{c$I5^}Ibdw@nTYxte2CTQ8-|{q5Z+tMsi4 zVt01k@-e;2bJaZKq|^L0MtTcVRwR6M(9Jhp`F-uB9WI&cwdc+Jzq)2u)y(k96<nLr z&gopv+v)sD%vA5_mHbEFjk2$s&ELK<WW$7-`e1v_x#siFf8NLCAGhPdtlo@w?_~G+ zwJ*`RB~;_ow8*sR*`w5zU(dyO&OGd%vdQeWuWxRr*7>*B-iM!D6szfdsC0dt&%M`S zr#E{!{Qf_8XXL)fz1M4dy$<~Rx$W4y#|h=j^6V52Z;7n@^7i3ttrP!E_1DYqs(*iR zn)>XI&mFICtU7+B$K~!S(W`yNC%5RbKdCSNC$hC>`mI}GI!g2J$6Vh!|K5LC!u4Fg zW$*RbN6+8e9qq5cxkP@?<As&Sr|!J^J!ZYQ$DBDK*CDMCO|dU{2jij@RteSQfx2}s zF2w#W$-i@j{m;VS`bj6F%_aRU-mFNQxX7sdz}&dKL7axQTb`)w-lje)PQTw|-?<Ba zR%~7U<f7$ep455l_FvxUuU)74Jn~7@x13{rYd8HmP`5H>!-g%o(T=Oc{XVR^oN?uN zk)h_v0J&AnLN%AB?fG}vb<^`7mgTMHyRS@`ojt8Ue)ZhtX*rV&>;F8}%dYZu{JAc^ z7Sw|Hp2cZ)`M>9e!0Tme!+h*s$kiO0koEY$OYTdBPlPP%{!Ox)vP<>T&S}!Yn}1Z~ zy%zHK`o2DB())S47p}S!cYlV%@?)!xt;rY8%T>9a^>%6bW$027^}UzA#pR3HSHI7* zZB&_1_o1)-nQZv8u=~Y(;}i;uf?^?+*^-D0;`l1F+{P829|DbE*Ie7#zG7*+jk&4$ z@&9M%=p>sjJM-;f<vq{eS#1+z56hYAOkVnH`7|-@{~u>>E7_X(agx!_^KaR<zdhdP z_4?xc(>qU}<P=$%e`imK<jfn{3l>)F-NthL>|^)S7e8xqs$``t@VCDE#Ps#VxyC{h zA0%^hKYXx`v;M^T=-_F;rEab0vAKF{$De7la^27FTAcrs>)9kr=h?5@8UNq=^VTSG ztA6Uq_4V7=XdIp;C487EGi2}i@}6i_?lQIWMpLx)y8jrh=$xWE;q>a6>dW(|Y|Q?) z^VH|(9g~-S-##TLvd4Q;?7pJVSv6Z6`sVn?9a>$!R_IgJbE&vo)+pE-1pft_B%|-^ z)Rlcd_NrLK!@lCU^XES8Vo-0IRY+a^swsF4m|qubtt0NKipCpa9bGK*zdhADxgu|` zil4WJ=U!XG^0uCs%}S*kll#-ZNvm8JGd;IzkyZ4o!`Gi2dU{&%ZTY$#7BkHgR!lh^ zf8g%B)LhNY>mKi$HnsC)r{FtbIqTs3ov~74`cL%&IX!sy{Wvk<&f@tYoZ-g5d8ggt zJo-h+vgTWl$&XLTmc^QuD-YEwu~@78Skn9B!jsZZpO3kex7L+<pFAzw<9%-JkJmfC z$nCuo>;Cr4?$tY|e4OZzyyD5@2RC+lUwU!)=b}j&Uvg8{@4B?iN${`1gZt~l-%d@h z_w{tBT3Kr<S&(~b^NX7a{|&WMPtIGsZjqd6*Xi4_*;Vt(-rL*dF7An*{b!H4PN>5% z-i!x!;&0#+KnJ<f&y<2DF2uv;FQ%aMJfi+1X1%Vk>8@(|lk-ehhpWtwwulC;KMCJC znfqS$_0V6}S3JIYbeFPyfo|i|QYBf|{H<?wwqM<K?ZTni-r}dSy~RM|JRytDz4_;C z(|shza(#4moOb^G3pWeSHt+YF_}*-d;gr@#3b%x6!rocy-Qp^Kx7O{V&7|PjTh(T) zy)5pX@_FIqci!?DVR8SiE^7K6@pjJM87g1xW-M2o99uZu{>Gj?HYIa6&ilP&{@g1k zEwpZC&ot}yI~a5GPt4SBE8n`G^SB<k|MbuLU)OdyCH`DA{o~KCdw%VD$enus<mYut zrr8r_*`B!m?cAcbpZ?`r$1H~oBW1r_9p#V;o1kPlGV9?~&=SS=HFoZepebRK_dATX zgNEHn%IzI~A2MSWZRvS^(f^8%$;$2jb?2$S*ZZwoy76(peBPCqd0E#^t)72&|G$ub zHdQxP+uUg4d#ZhMdsNbt=i9Uj_m@b1pKA8l%d4pUdF9soi@i#Jt_kLMF8pm6f9e(U zZ{E~->^nE7PWCOg)v>ghwBcU-Mwhr4ljEDCJiaZ~J*Zw9bk6#gj){5Z=8G)5Rz{Yr z+HdQwr@HjWx2t_upH8z<-8*a7_vNNWf6q6+`(*U9_|%@=>x<>1@4oza*63&Qf6nr+ z&r-QgZLfX3yPx@H;NSNvw{F+_xa9RFCd<bu$0no&soX2yG+|Zz*RwY#eeo~5ve`Uj zqV=oz$DgOo^A=ml`fuBx-D{65jXHJud21aPY~km}<`)gYpfuVIN~6CITF?6jtKlzL zST5ZMso}k3Ua;e-;S~fna@u7{uKsBLeY(xfP1UX!bHt?9bIXK$$lI-Fw7@NPy<F(; zH?h|*g~h2iX=qF_p02a!tDfd0?R&Lfcb=ZU<m`)yo8I0KET6f4Zu=INCCh_V6{h}q z5V~{5lqaiq?S8ZU_Pc4PSh;uitLNTTaju*Bz>e=KsI$Arpt`$eXHx&{<=I=4J~@7m zdY8P~+IUOL-0CeyWY6bb+^f4J<(|wu*L8na&gMU{Vg7ZgQ}r@80$xo&ldti)e8cM- zk(qyI8XkNns=IK-`u-NB`nNBh-;3H2YZtS}*?!7H+gBdPPjS9Wj<+_wKT|n<i)81O zZ$G|HeKJoiHTL()|I!<Cw(qg|?jK&*w`OMQW93{|)n7}};nkDvsc-SwpjFWcUoE+| z$nSalvK~~u|9J70sdsYltJ&a%;>(XT*5TO}z`!Ip^Fi?5f+n9grU6_}PP`O7_<frG zwCQ!TUx&`S*R<&4&ZTvR?{E4|dvja)___Js`JdmtPX8jS^0jL!s5;iZrK586e*Mv} z*%pFN4qm@o<-MG1DsQ+wq)<K-@a(lhDbuBo+|QR@`mntw)O3pPj>?8D8nM@6w>^7s zzvysz{EmL-t-`sScAK}!PFnc*f5GP!Pj{-_yXK)Ke(tTF@ome>hnu$iuGuJKQ+qIW zYu3a&M!5yS-#0$*i(*=Rb$R!b2KT>WPq|Kp*S?DMtND3-%GXbq+xFi*`|Px!I`_Ms zexK65$9bKP)$E-c`rFTA{>9UGH-Eenv$<u@&82QqTZ6l7)9t0MJdpaCe_Kma-S%5Z zQNP!x!04riJ{kqjmekk_pZRz>b=&u<%|C8dm%rvzY5dk)Zu1vZHvF3RnzXQ<{vdc| zg5u08ufsIo82;{&JLVszJN3WLjHU8zU$#4boMiNNn)RMrHCgw3BtIT6TK_45%P9Sn ziuL>IE7>PE9x-0N<XXq~=FCg4rA)0ScwPE<=h>TEdfR<uUPn%OR`<$fuI{Z(iPtkv zx6jO-U$VzDZ{4g(Qx;BIq*ecE+s#zQ`Tg5i<fqQL{NwP?MRq4%N58DGjP<l*E&drd z&ujhix~oB<|No`#{j<mL;^qjueJi(>tO~jk@T}4x@2y>B%`La{N|l>JRd=o~jC7v4 zzPVHS|Fm}X_2zS`cKN36+?*)sJacOE=36{AbN9z6?e14szc}^MF}vIV;gh+FfiK>1 zW~V&6_GIa?S(&Gsd;O9tH{3qHb`q=g<n(n}!3#Tj-aNH!o;Gz~)wPSzrq}V{vz5O= zWyW!eCgcUDJ_uHeubX-2x0&WV>;CYVD{qXm#H4m_`*XAS#I<Vqk6Cs1=FPoSeW*O{ z=|PbY?GP8S4Da-%_v-HKiF-HqU-7F)4gFK^|7mnqwbN5Ie6nI%#^q15&p$aZ^YXM? zzazcZWre+xD!=RN&bM=2x%iTep9_ymFD<`1>)mE`{g??{hwqojw`Zx}5~`Wf?RBm) z<C7J0;LoOyizcPr3h{{ib|%2l`bN!*EjDJQ&1<}EGA*t`JGS7d>Fki}f!?W?4+ggd ze>d0G|DP48cy?yZzPPR7aWl3awY}Z9YlqLjIiFt7J*%SUo#q@J#y4-yzQAMaZa!bN zO6b9{FRS%?y~H;Zt*rU<Tdy+rUs!nT)+g7T<h>`YjO?0Kzvsf;`_Q?m%~$sA_kz`d zKZ1GwTY|P0IIlh*Xz=%eEWg{?ieAuILQlB+xryMSx_get#bA65-m=CH&kvdtt=Id! zES^2(-0bt(YWLT~>-L|Sr4_#ST+6nEs}FwfeOGU&>A9-r*S0zPDh%(MORv{WzIrKH zQhV>g>Sg{@KF&6O(eC&+?cyY5wak-&y<Ss{V(foR=QOH(t>!#)fAja-rBmxnU+q{e zV_P03{onfC<1J-%Z*In~+pnftm~{7`OXlahC$_9&kCp4?wBvfb)Uy86iKiz|dsIF$ zxi%>_YO21@Q|_lirknjAt>pgn{Nd$If!=3k?)uq~%_e{Tm+I$d7t;P`>a;!%p0vm2 zeD%Bcc4c9pu5jPn-A^KQQn{Z@V!S?WQq<8;UQRXO<x|=-U+lL3_sNR`G?G>)b0gr& zq=d%2&0C+IeD@_{p3hmiC24}2cFo;c5gApp>BQaot!Hj@%qvx`zV^grvA1XVlX>&6 zotpEuJ;rxc>JhQb(;bmBWlyZ(hegT^&$jP-Kx>faUVW}{qwYf=yY}>&xu8h7RKEQ~ zY%X|s{*e0lx%gJAF)ns+QTZWqaoXb`<L@CF0T*smzs-1aZ0o0bWA)qf?kvvx<Ma92 zr-Z|4cE3L~Tx<W5v;EqhuzUL!ZVnfIt{*?`*8KhYS)QxE<nWtj>G_*yr)DJGSypP4 zb4<l~=FNlqrDE0n&3505^;Hd>TJv9YXNhVmr{;IhepyrFiL(}Z++UV+Tc>$h&vkz1 zZ_;Zy?Xp&`b+lhI&2{&r>65tFe9kWUT)94bY0|=q)6>`ADtBV*iA{`9I`isiq;dMC zX}foYMP+rGHs)z%t~h)`b=?!i(%h{rHlIG{_A<?5eX)7z?k7_#Pj3FPqHI-0r`ubt ztn&+}eK2$PRtV}0`1S3@SJ9{Y)HFRaswxfk!$Op0@$ACupiNNoB5M0CUFN^{@xfQn zMi=ova}QN71*dZE^>*d%xVs{ZEI}+X^B)8oxckk#@jJ}9sN-I(>FTq!#&u#_uXwoS zOyv@rbaeaO$DhOW^=2u*Umx-4^PI?qv*u=8%q{nvmveLPs(ssQHtzXyJ7)bS2d-Bt zBGWT#-%r?aM$=p6t!<@KRIi+3*2*;w+h(u6Ho@QOUdj#^-H%JBz4FRFJ<oJ?&d%S7 zhc6|_ulo}_FW$m>t#^}N|1}nQ)<yF^%U^#x-CR6pvU57qY{ON~PgyGW7W1B*ZJT{y zTbA2`df!NWX}%>Yhi8~BnkG=aJll6=tj+q!(D~2Xvy8u=xg=C@WtwmFt?Wy0dt*;p z+G+pR4CxHiKX*?l>!m^c>{%0E+}sw*<fq`Z8ortAl2D1;PEguhdiA-+UQmJIcL6lm zd+@I6pYWNW7RbI2?hSHyTOcQy`uskeoNc??=fbqLL7ZAE&9|<(a$%*tXAqm}_ZQ#) zZ*VT(xim`m?}fjsgL>rV+WKB8N^De-DP0%f8kc`Lu<Ka0e&Uj}x0yel9xr-ysPcO) z)7_H`Rj;3UXYZmVuJdMJ$k+AHe_Z@@JN}-x-A?VL8-v$JaL&9~KH=dT_oz+hit8qo zR)wupoL^?7#tIrtuGyQnb>Z9q8JFUw{&&USk|yz7Ph0jg%sa}m?%s}?X>sm<pIw-? zDCl(1%E-)D(>}!AI=6Y<&3#@|0zHki4rWX~zQ%mz`ruAm+ibDv2YJ^`oKqMXG=E#k zpTDzeKOOnf`)-&2)^$40(o+lN**6;(KED6y*q_`@|CfA>)mo<;`R%=!XlyOtvb$T> z!Ky-aUB~n(pw;_vQMG+e7y0jfJRrLEnEQJ1AF^`_Tfu3y*iGaGE563_0^N|}(A$@o z6R)$siYYAolzjf*r41FD>))Ndu`x9I-b?q(sSEe$@i;cl3e5QU_PfWzIXi2%ZTokD z@%kB)&)?pBsf^PPxxW9_uI4)-J9mM`869_edne5Lk?ysqd12Gh=IPN}r!Op(mddK% zQ?$}%cfquUXI?p~tzG0Lv1)3-nc9;NH>WRoCoX@AHP_NGezw=8tiWAseoyg=SoQy? z`%ABwBJ*ik>c_)wbHL655EH7gX<svK)2+fUJ#Q?E!ph|BUwf_6oM1eAivA35sVS%4 z&-#08bDhSw?Q(X?v-#&vIua}%<DDUTLQ>uJbINqD)*|oTMU_s4+_!I8l)am@#^2^; z)TQ;yu3c~1x#?z+K4fLk?tgW+j5OqXLHjwI`@NMHozST(<9GUg`bFJ`zUXf72>P0> zul7WxosZwrf4)+1p*y~*=GCC(R)OADy*odOIR|;TvAk%UIXU&sv8~It2c6VfRljO} zX0EpI_fD^GH_P6v$g!?+ITyE2Ba~aT@bBt43&$)m&8&z`TBX+Ob>`{k>qk%7WwGG# zwbWCmzuW%m^g4BMyKg)HDc<CL6F{qm0uStedn$Cw`z;@<^NP#gPl#6k|6uBq&o$lB zPxk%lwOtKfI$(Xe&Ui}es{=X@6+*syu9p4ly!<Bn#VzZW+gP!zv114A^}F}5u79na z`?;{5d-uR=N`h3+fHn}uFFDp*r;KNt-xp2`ql50cjZ3E;I`!ggR#X40>0a}GZmX8h zZJY97n&Kz(ZRf-ECog~g|JC}gmphqm`8a=O=bQRyXK-3bpr`HI<8_+STaGOLk_Q@Q zl@ig<%y`z%Tz~rftsdvqTjRH?%<y6f`fsy+@$++Dwz=Qt8Bf=%5^K+y&oX~P;H3a7 z7We=sdl6eqmYDtS-3esx3vEyquK5+VURQq0`)LmiIaCh1m#$kl?ZGEdQ!Zy8lj6(Q zmARd(>(9^Ax}P64{oOaa_jiKx*2J8>aE`nC*$H;-X%eTJY<FzS;FPSstQ%y|yYy7e z*408iZD(gpd6#;;X6xGN6CW)0{vhqg2uXE9HHRcsKUdE_dVWvqm#Z6o$t-=Z7bCj{ zyz8`Sum6tq#^8~#hu@z6!;{(?8uT2R`t=t6Wt;V2v0iWgX|?m)GL)IK<FpHB8dq-- zyYX(#+}lrD??3B`a?;)#y?&Kx>W*m@Pj-T)+P_HrUmLTt>gr~T*xyS(J(;S%>z<C* z&Eq+@%U><w`Iow9jn|$0Fw@xQo*z8L<;B}Q9AJ}80*$w?^hEy2*|m0e<m&Sie>0UC zg@E_rO5WNjaPJIwG`V%cq-A*KnHU;NI43-Kd{Eq{slTpr;}4sn^t)`A{Abr{DB5k0 zd-MJKk=D;f`QQD17CaRSJRRMd6zpmKnf;z><>vHq^YT}ZSa<%rEx76X6VCF;6=he1 zQqRX-ciR=ZDlLM~C;$JApT(OB-s@GCoGS@DU3_!z#0SaS4c<lz!dLlrrTusXuJtL{ z2X%sFzQ^(P5ep`mXhue@64B9N-J-gN>)8oS&`}4<QJcEGiac#<y~N(%R?gnCWZn{i zb1Q`NwjbL0Xla>~;q@J%Mi*x_*WaEL@+5zb%8wtHAI7+G!dGGh3Ua=)OZ&*ZF7$PH zWNqJ??H*?XxWH>?r+hzmVjCoDx<wW1<7s_6902Vt|5sumUdW{K!zJd#qD_Zk%^nNu zKcGI<=;q$h-Ref8yVXHwVvO!qM=DB^Tovwe+8w=D9XV-TT(7A9v6l&rIx~$tA2s{8 zZfzHho39$a<$L1m#pWK@&dfY?OFU0buJLsEI<KYXX`X3sEARZf!CPl7c(UGp?VkK! zzaNMnT&TYG$oY5cUW1P;vHI|hx$^v13%0uHAL0|Ag3g0zwdeYRuZtz%A?8uu<Ujkp z+o@g6ofS#~WsOmsM^pU%D<-~m2aWf)F)x48{BG-#?5#<OZ@JC+`wvX<?fi6LNommH z47r&_?!x=$c2ND4kn|_BL_Zz5BMEj-cy##$#Zsx3t@82fZ)u<Zw=1$J^|aFZ(lvUr zt3^`P=5KIQd7QT{?aPdPPaACPY8QiMHO_Bet+oBC_S@~u@bg{j1ZAo&dHs3wZr8ix z_l0*aUAtZWO!d0s7k|ah3BG&^e8PpF$qP<AZTus<K&PpQJlG&PIpxl>(v5j1e;n4h zuslll=L+ZV2HzB(UhDs}DNb$8f9s_UduIs0fG*fvTIl}h{&|Oc+dXRJnL6h$Ram|_ zNafek<Q;;gnF{AURgO*k=z8U`M>?LN1qC)1r%4YM&$;3wve<mh#NKv3k4slq8#){p zIIKHCHCSzB+>#YMD+1Nu7=K@~Ex=d`>Fl4N&n{03)81CDnKmK%*A=}?t78AG29F=V zXU_YsD|D~@%Mtxj8|PXX&bozin}ctApDz~TEKI5KtNrvs98XXvC<tw0kvG8OWVA%5 z@P$d`N6Wn_Vnl1<n90Ctr?_~DB2xCj*X&_rang5a@_*r*DTr^KHu3_U#tEDg9tazG zNjagp7AJSq8>6v-GjTv9MibQ_%i>F{4?ce|@v-7-9^Te?g@pnCK9o1%A1y=b74Bg5 z_)r+UVKIxxhliHPWe1)zW{#>uQ~%~ig5T~<cp&^u-~t|{$OYN~1&5~o#%5-rnhG{# zaY(YlYDhY$r?;k}Wv{u)j}}{&5<E(g^Q{5Lga^qF9yGAX&%<ggvRa6r4@5Gl{IJlL zOMK7EX_v^)C<_rLXde%g$`1((3x$L2=>*k6{9G`Nfz$3>m-~%s=O+If&JOGlVS@HG zGjQ6m@$q?lcqmCwEyRDL(F6~u(P#ptveDcG3I%ZHASX8o1ajI{e}6aCH2d0#oyE^{ zLUlq`OxTif(dp5nM>%`@g{9RG9`6iX=#Nyo;-0e*IL#zD`N8I*r&^Ui9=1QdwA5Qt zXgw1v*NN=)d$qP^T|G77x8+kcl^+(WLSLAPE5tm+I%crUe?G4|F73>Wi3<y}Z*E$u zeNx>yU+UTHi4TOIF%(pT{Z5cjnCfsu<wwbz8=ii1EGDk{{Y~!J@#Cl0MDD*;E!@7< zuc`kqlT$f7DDfGm@QhVS?O^(a1qU}(etyQo8}atm)~A>K?LDvC*8hAeUiotA|D~&Y z&&{<yUH|X1&#qe=sth^p!j`UR$8&ZdQt+nqfE=n5u^~XN{?Esrz@N|c*6;r}YfHvO zrMusK{3rRlseND3IO)OUcLkS;Gf_bx2sHB?xcJ|%*XvV%eE4#9MRI8VjSat39vo;4 zTRr<4x7tbd#gnVAD0qA@<W&_VDijzRc|<!_u*j<#SAEI&`C@VZ(^pqlZ*#m`{eExJ z?cD8cd;U61syQ_A&SEy7CjS$M{hE=yh&u%}IP79kQakwk`F!=0z2;@*yk##hrGC2P zt$*_2;r7d>Kdy+2>&1A?tNWFid1*=K>nlq<Coie~{-k=@I_WnXr>e{^Q2oBbaN+~u zoRpn>#3ir|?g~jlHM7ie)tK47h@0;Ao2C=_<jTt6PrqL0pI8&Q*-7d0GGFP+KOc{) z<loyfY04Cp`E|cm)_gv@eN)QGM-#TZzuB>0WqyI$_Z4=N9z2%R{6<_us__H2OGs1y zqg7E`SN-|E|Nq3Ir>Ay)Njo=3Gk?#=WpnImtu)Tw*;#yAx!*=f*1D`9c7OHvyvlz+ z#ZxXX^Iey0EmRY7c;BR&Lv!ye{^i=#AC%xsTva_s8FWZnm!6)U-@ARk^Il$Gzdq&M zoJc-78x8$^KPIh-*r=p%^ViqcC+q)xme-8hvLa@0Rp_x^Y4yYH^-AJ;F$LR`!<+hd zHKtGM7oJodGT-AvAXg4?QQJ_=qGWW?{rT$X?d!75=07^xy<51%?q9`@m^~GN>mPj% zSLL`{{oeM{_WbyhQ#2>5sHyD|Y6)8#^)&SUITra+<Ec+JXI;(Gz4$_MeRccd2A25+ zD~kQ`WmH6nDC9PF_<aaGd}^xpbelgP4qw`uEq-02{LPJ(s(=54Z_B^`uK0?mR>%bL zxQe(_)Aiq5+2`IWseHY5`;#*>jqP5iyEL7Db+!J4@p+rKpZ`h5dNldFbr;(cXZL~q z4oB2~yf~bFb=6dxpHGA*ndistEO|Mp=G#pUN#nF06;;)pt5Qx*a*e*Udj6f#>jBq= z;+Ol)ope2}IyUY6ytU>JKVN^xB0t}pH(8O>F7e3oYeWV&KX`q0vbTpv!P$Gk`|E74 z2Yi2jU*9<WT*=$*`S;_zrf5986~D7+s#*7W?xGJ59ItQO|KH=o$K{7DiSro)<8IbP zS_j<|Q_szbtor^=x0&s(;?GY{jepFwD0JHQr)Wx#w7K89xV>2x)2}gd+GU;$=p(Yk znjpv&IQ_xm#ZOO7^}e+wQ+I3D)gsZg(cAr=otdefetzE7)e*l`4;~j1o=RL{#mJH) z)NzYten92K2f|vOx9}{cMkzKp+!|Qqg-wNOd>Sn99jc5RB?+q;IPKK-sQj40P$-3B z8ocR-;phWlpb|Rs`ntVy>}qd?^<3$Fer|4b-QUu*SC3ZM`DskCUv>QW0}O456pqAd z^biKsndSE?-H-MDR&=iY{pF=Dzx|(rwtL2R%#{zS-w}L(cMc5^M-73X!Vy$B$k+Xt zcxJY_zh3OF6dBi!{zs1<EeTy6R`mbxVHHJ1#UpF2cUub8%#oF^BCeX-s4ig0X;+ou zH`hv4-EYnX{@kBur|Zvua&q$VY1`)B)8Mr8>lgS-RH4=wA>6TpWqt&AfLs4`miawm z&+Cw*1NUrgLp`HV&7a>#?nDaJB<VfjksvCvWI!{Fhnc5bysLareeuzq^7uyF5SBlP zZ(x!CZ)aFOjb;7}ZcApoCl5g)`bYh@ch+att-2@u!>iH6iBt26%PI{<9yS+655bA| z_QVOWxa?2}-=G)~Ah3t)j)up_TN+h?HI6Q?LRnmuJgzjDyfjs85#Ui=BH;AQcE9ZR zXLC-b%t$%sbMEHnNSniY>C?B`?dPqpoxhbsAftehaXy=VKzX_Uf#ti8?(Jt_WMbhE zP;h9#$+)nup~3&Ro$b4D#`$vLMa*h=)Iy}BVjK=EKgfOM?p@&zFY^lDD?^0wS+hl7 z;lbs7{{6p04=g`E^-cpPUMD#;Ff_7>3w+4@z*JiPouy`1^qt@MOotlwmYd_pmiLz* z*NJ|3S*U8BK*;9~3l6ff)JW}dxO>-@asE58?{)-rG&uZbVzPVt`^%1a)|#rAle{x< zhAS*kMx$hSM2W+4X1llc_tq20PS9vCadSAZ+_>tUDwGdRqS!TGILgFi7t8bJBX(&t zBdnAa9$c2R{Vs$rSHtwYNMdB1ugCVSf^gDiU}TyrDDdHB&%A1a*%MT%lx%Eh@E2#U zwgGYQ`VW+!&v9`4C~-T#mnbjpif}lvyt~nM9>LtD;LzZZ&cae-wfK1)QC^JIQFw6q z=m9x@+$AC`>=Yap7&9^1&AnKxPm~w+!~{OPOiJiiM;L(Hk_(cIjPuXE*g2gjFN$+> z{3tQpa9o(EIPYm_@K678#~Zip2p2BsW@V|_C2`|1adAEpk~Ju&AD64n5ih?;T&S$6 zXq5l+fL(iuFq55j%Uy3`f^wFx!-3^DwU+HCCPUbIIUHE-KJ|9OsL2eBOrxQJTxN`h z#%O5Zsuf1l#%S6YO&f!thLt{jcXjxBlavz^Oe#J+*z#)C>Sc9*e=VIJSEaf4!y)d> zb8{^3-k5xJy_8{+%dbDr?dSjc_ICE%ipRa?mt4hTr_8JS^)m0hw{2X-i-qmW%J=`i zdvvi}&4+^}Csn5}nKo_Oqd9Zm32WT`ezmsfo(rK?UluD%O_uGeI+2Gn%lE#H&Ft3Q z<}o{eU!~Z)%-5=dA37JDe{yp2<+JAZ=cJvPaq(JesrB{c{_{&tX)a$9xY$kf{GLZ$ z+Lv;--wl&7zkF_nVe+NI<Fer;mA}5cd>MWJSJ*7;^1Q-zH_O`iWM}o+|NF7%_Q&3u z-?#7U^+ms5v-{nymv8gyt7n;H2F)_f4tqU+_tzac_rte8s+1(u#?DYxcyO6(NnE>O z^!XbblP@!}%S^ZyQ+zhHD(B3S;@@w#FQ1*aOLOn1Q`)CX^4<0C<?sJ{X<qfa$QZlC zLoFpgpH3G)es+uSX2rQ>w=y%QhK8NoWt)F*Psz2&^re}XmziERnD*lJt?c!4{p|mK zdGzPa&CQoR)#rxDguj2Y_xruf`L*9p`sqe((TJ^h*s7Uk5EZ!A=l=CBqb~%z9TT`Y zemoH>y~8JEqEY*4@_e7!Ih#C}&--;Nd;QY;f8UkAY?IDg@!BX@I(JK;n8?e!yUVZb zKQ33TGq?KPPR%CQvp4)K9=4cNeR;9w^+uWSmT0+(2aRbmUcU3Kr9N~<E;?uT`;F2A z5&oaY?f*^O_xtYqYc}h5^PT!uTRMIAcY@urrJNi;bbQk+&mYgRRi9Up6qUVp>#Z6m zoxP%`3y$XKe13l3-?@#)^Kculx3XK0$KT)I^FPmv&fBRPTY5D#_usZP>c79fp1$wf z*7Zvx{%PLw*55ltH&yg+=CN&;{j8Td^I0x=@#|`M{9N6_usIoz=B9G~IMU<V%+9ap zCcXLloce!1j~0IQJEJG+T^YLFU9NJ8p5mrWweeToz7cBa9_SJh_^@(ujgIKosq1T0 z*Y5lE%GAGiW?|m$x7$jN%a$*BKCgP+v+aKroWyVE?asZGy?*be4gPg@_v0#`PJMYT zI{z&H^y=y#58Ic&-G1M0w|yFi)$=*Um!@b2Py1b|{plEo>)fhWD>I)?4L`Tbu<+3l zP*z)WLhtI=yRy7Lj(C5+k=(!K|G(eq@8fqAB${<!bmq6cl2(7{brZMVil4e^j!_#D z9MA52YN>Jg`&OIB<@|(F_X{pY#`#t&-5k`Ngh09RtnmEWZ#PRm9u;4nd3o8@Iq|m~ zb!KmU+-JS4ncvRhsPShpZ~wn9-0i(4tNEVt*Zo{}W`^O*9gq7?zw3Vf_d~nAPx<}Y z??(2^1@m59UhZ%8bV~506@iO4y|elI<#OhqA0J<Sy&j+6^6u(V{?kGqI>nEz2{bhJ zzp`bo<jh@K4}zDyU#-XbMSxI`IzS!dXvcTEUaxzpEdSHv?B#hCk2-UfO{x6+?CPBN zCmhdg`n<8fc8%H-;fGTv``cNbool^axj*93EXQUx(am>{tZ`bu=hG>n>n5i=Ier}J z@vNL}obDH!w&3La>m0XJd4H5_IUg(fLG$wWtAXCv1#wkn&;q{koq)iHmCZ%_>;7Jf zOrJZIjZY@RM%T%9V@x=U|FrG3ADZQT*kwxs&c2-<UspNHAkpcpCHqs6jBO46ma7v& zpr+uAsDz{396zosWV3$1r+C|vu&;~P?S8kb<Z-Wg*!-wxe<RktK4W~|$GM$P_g!yf z<2lRca~|FBt7K)VITanPOmK+ww35Pu%{o!JF}GKTuh(fY&bhJS(e;~~)6+M9-}CKO z_R*bB@7~N@KKGJhyG&5=@0H8v&5En}cyv~=w{^#a3vSZ6Q?}hm>fZ8fR<_vbsH2}^ zKKA^6zu(^W>y_Zli;G;nt6k3ie0aEhd9V4s9Twlu7@r5{4huPh1czH?w{tI7y<R)L zjYo2lE~i=Utt}>{udck=1uBBJZrLJoUH`S8&grdJ!=jh+n%z)XyX{ui%{88e-a$R; zhx`ra_37D)8SC%;efPcIE`#D{XTDr;=1)ET@iw3Jn+a~cQd4`CYm~Np{qeY8{QCB- z!jJo^A60kkfARRV{{A`3{O8ZBbeB)wubdmnWH&o+*GnPC4dQVX3oCrTzTdd#W@5W+ z*dH-#L*o#g+taoi)I4Zp|8#h1-s``$Tk8UC>c70WsHszQxs_XdnMAaO-#i=3)zz<m z%McnOX*A;G_))TH*6TH!{jA<@xtukx=;_q(WzqS2w?4b`Np8)SOI|NeDEIpao;q=R zZuz~L?{>f6_h-Y|T$cWOpXYsF!pvu(04i*9<0j9qdAW4@B~a<6<~wW3{HRyoYTsAK zTRrMf?z+Ez=d)Q~X5as#_Vw*;bJ>aqjhT0L6h^LCzGK1FbBp`!X6fyIvq|KA%rvpY zg%Rs?m@EyqE#1n&|M%zldcXbuzOKK#CUWz%vdA(!Cja|?o|$)NgKPVLKc6o@Yj#^l zw)l)8s0P!l>z?baw^PM;j>X1jF?AcRMP)Dj`~ALsucWb>{nck?O-_OPU$5V9$Ng_h z<;DGedfo?~+y8rPpLNJ9Jf_ff*V}EkyN=6OzuB1idhK?-zWZ6vPpZ%NNuOW)ElB_M zs`uLUi8l3bZfwlFU;EuxP%C@AtH$f;`~RfQGD`KTp8m5JxoM3p@0hG@Xz*9}-VQ3W zocV3HRCuvifT}W)%{^;Yh;2SEQ+}uL<=po*&y7kS>ev7D7TdRS-pQq3nQw9ZxKnss z_T`Vq{r=K<I}%@?zHKwlzP|3)OaJ;?3mpP&6w=;U{(7<aHZR}Sx{G&q7GG`^kGt{i zjb1KmP2Jbk@k>DkO>5%oZCe`7->ZH<_uH-P^&*?kmTb|D*q~ti<8}Q1U00%|OdnjH zdOtkDCF0EyVSk_e`ro&wXI=i)&QW%^;4tsa_}BLdC!{oSfe$ZJSo^JBt;kt6=a1pW zH=#NUw=Rlm(YO0N^ZXXcZJiI!eLinLpEWgU=i+T?hju=nSAFUG{{MfMmhE5La9(Y8 z&LrK%A+^Ce!D5QbXV|V-``zWl^VRWxUq$OJ7u~$0=8^FH3}YMN_Z>4ElD^IP_xE@B ze!JWK0%vzVpEo<nY}U4@XvX<3idnjDW#zKgY`dMe`^(4v`kKzu$^}Bk9Ih8M(&VF} z{wJ|5T;UnLb{^w=4v(jsxm|5nrMGmIAKxChw<JpEwprbuAF8{qUcDc_=R>mm?}%+S zQD09dOKy8{xku7C=uDRA{d^tU>s2xz?YHcHzi;=iu;18X8o8CR;3X?djaB3!j+JY+ z->W(roV0Vn=X2KUE#AGGTl?*%`q3VvtGY~j8*^?vIXSs|W~<VQjW3tYUe;gtWpO0K z(qF&2^>$^f^lsS7_rqtt-QEbN%x7n2-mC_#pi$Z({QhP_Gw-E??DBJzHU!;GTDfZe zpC{^9muG!r=arhW?_+QNl165BvFGvif4`Qz-Fki5j9iWQCT%8d=XO5d-EX%=SN;Ee zKR!m<`n=6&pI_Iu?+X=<JE@-``=RrL$DHoxt}mW_-~YdM+neAX?Oy_llc&emS?;&( zl`_?u{Z>6~&dF#S<!L&*U$5I8^u;t^@YBuW^R}12-Oi5}4x43FnzhX*;_KF*wqJRE zJUQR<_LTPeIeI%DG`$MGxh7b!qiXfFtM{*)-OibO_S%&1%1-XrW6NW=J-M5`e($vT z=e7lY?yq^GY}&YnqwlD>u%k7>tTn~e;lT3AQ*B<a*?dVVdrhEC>Y*0Sm!D4S>-X(0 zR!=)^`MAe8Yufa#)b6}JACHN6w{N|wv_r`FLe9>o(~h1O%<Fu<rPTj*gMZ!S`~Uxa z*I6TMd~M6sq-#?n%yPcGxOnuu;JqKWuJ4<sq;e&!`@^wyyWd6q`ErO`KV>twt43O; zZrc4$$$F)TDcigC_gQeWJU#w%Tj>FHd-;Ql=l}on+>~+dpY5;SCbr8?JJu^5K4+F$ zZj{+;-!nQ}wumv#Kg@5x=h^em=k4une>`Y5eg0^{MJBtuA8WVH6V%>!|MmL)b)2*R zxU{Uc`+f8LvR?Ch7WPw<W^TRz@7wmH8)XT#M=rbpjn}V9Xfa-OSVh|?M&RH?jtNiR zuAO%K)Ou-y1P8Gfk)>BdKL@`|xBnfv>+iSQ-H&UQuWj%@vqac&?hM<dd(CQoeE9Np zef{1vw%PLkKD1|ETH@(_N6gz`Zr!h!n)7AV54jl%pWl4mZugO`YyIEs`~B|a0cL(3 zzvD)67xzcht=_d&^h4*&_(yx!u3x)P|9=0|t&b!tyElaED1Qypofc4i-1=T%Oxan} z>nWT6iKzeEdO3q}{<|OhlD^GdQG0LSkE8m_W;m|gyX|J$>{P}1e`fuDw_E?HPKXrF zHVL$^r_l{6YPBREPQK>Yw3Ib$+P7H~mEF_4opj%R-*)bJkgH*|Pib`e+|o?~-a7iC zx%c)|zT9%zPkY+NRr|KB{rh&?ZN1qi)&IYp|Nl?=uS?VSO_3M7y^YCkdQ6e$EQ`XV zLf>j@P*%IV``*`ek0wRry<RpuPpj|q+~ht>wO(0kGkYKN`!&V8e!W`V{rFqiuP-kr z_tqZi5ImZ?Q6%Ebnc%y(xPFwB=oo&!xUgL=%0@5k?a!n7b%8S5w{Jc!SH0x#_xt;I zJjt*B9sTQKe_e>obL(D_4=?-PYwx@JzV>~!D}zp2#jh{R?dL}6pPl;q$?X;21>I#P zCQ0i~3+k4fVL~WT1c)&*&i4!1RQ~?n%ld!M>$B!vxY*!P7tA8{DZ#1k-P&!2-aco< zg722yt9)Ma;UK&Ca!`A=<mJ-oV#o8h3MVd|(Ys=ozTmnHTWwIDX)*A#`FLbY!ojBP zRj%Kv&#VzPe)A+Z^Y5Q?<@+YH@k&jJpSJJQ)b&dwjnhPq=WpG7-tPC3z2EQc-tlHy zbe?B;Y-wnr%*kw(X4qK(*14=TNz3L(aIMjfW|^b(V#@aA^Xsf;&(*L$w;dD?>)vap z`F9mQ*WLH<hZ{l9UYHCne*eC&`@VZ+{r`VQgV*i<_p9VEuQ|vu!Rs6RTdI{>CT`bW zw?pY}+3nn)iS64AAN!xVGHH86)2o^3^G@<_&VAi*{!-enl{;6yO1QJ5@a2ZXeA?%1 zzFcsAx##ma?Y?C7CR@P|C!^2w6<_jHf7&j4D{t@DYcFpk_v^09&Az$p8sq#3wm0_w zKidcA?B3KXXKOXL;1K6g?OBQsF2~-#e%$`wN0H5*Pj%DYciyaMsVNeSCJ=)!I>EWR zswF{5W775rHmzyRuQFDLA5c%bc(dm7+3xG$F5S=P^Y#0zH*LNiSG^R}&fQ@$t+(sx z;#JYF8_pl*x1V#)_WPYz-nZ`dr*2RBxpvQ1(GNEZ4)b2tTs~(~{G@rHzSddO>mtWx zZ+6ZKVed@x+{|GRy8RmCe2&R2$+l5C=AsAJ{{WRCXXAVFr^l9Q#$D!Nod53EzC~(* z%j^FB61CeX{Ixs(qx}CL?qXKaI|>rdzUSjWt{I_u6}c1RFcTbGcO$H_o}8F?G+3_s z%|?^DKR+J*SX-tPBAhtkTNI1as>9a(HlIAs@|YLCum5|!e(6Hz_HEy1r2cz*D|@}K z`Q4J>v$boP?Y`fyuiy3I5O?;v%=ZbibGLcA%h#3^dPXxYoN-V!t!QO>x5J6$c3)SD zcy~PoH3t$8w=FeHKDMOvdaU_uR?TTMqcWFH{S>}+cWzsQe|c>@<JWoL_o&Mj9uYJ> zeKeGL-@jk4UxM0){dT`Hc74BBefi1B$*b$P5u8W$Qdf9z`Q#+Ix*rcSf4|+XKhFr% zOE{`ux6xwV@pJorzniVZF=hMi_xr3v#n<`Iw~IWnto;4GpQ@!d{bm>_#@79OYI^=v zUxWYakJm~T2(zotuPFkJ9DQu5VyOv>ul>5l>?5y>?6l>kRR@+^?_YmM->KMLwlt(` z!IkK(FE1}&?m1a)X>|Tx)1yUsgv-&{4yAretdCz?Z8Z7R?BJC;$7lE3e!KB>$LfFE zT{b8?*;RHY);=?y@A>)p`SbpzRn>n!9$&t8+pSd<ey<<+o?#H#{Nl^o+uL`fott9` z>UbUf`00($91BCxV2SDWGWP@N=WW0H{C>MV{^PwF8<)z>OP^Pn7Rzi0>Wgff5}|YW zYFu}|(zO4x%=6>EiGN+ix6TCOtE}Tc<|RK{mAn0}S?<oK(?G)#>;6@C{^j`o=g)I{ zf5C>!gp(R*mOS#Tpt9Q%<8u~^pJ`r?udm(puuVGaT<Nn1v+w^&>$;it>D%l0|5ktR z|Nk3bHzU{I{_mGr7d%=bm-);LD)zPI+ok-#xa`f1k~4<KMego}HD@2~h_jwM^I-ac z2F91C<Np;&+0|6|mdjPWSeSEj#q;^~c4uc8DxaNeZO*M8+P>DUUv4g_8}MkK8~+a% z_ua35YXwBT{qp*{e(s)+$1*4Tnw|W2@7uQRS6ObY(%=7LQTL_V?{~v%PuUTw1T(Tg z<@_}LH1nUouJ50x^kVY%87ox}$qT-Fk<+bkvhw-d@Xs@f&snP5emo*<`aJ6VC*jt& zT{C@lDL>f!@7L?)!Tz>eCGKBY8EpLck;~jNE8!16x7YvP{-u$>X2A>3Ge#ntHEjQU zIDB;H(ziJqkBYqvp8qT4W3e$y%`T7^?j@x!of_u#P{KIFHDT4Ry<aYQgQ{5XKw-kI zIY);B%lSg@!20)}m&{01PrLOpr+ewDq?4=mxMnC$V*r&Xby~OUejJwf1GQxf-@$T! zVa)ou?>K%O$<cj2zrOC`Q;F;|6~VULejk(7L7uP#4gIu9=SA%KdrEt~&-Qy&*@buR z`LQ%BDNNa(#_^-%`uC}`bxxNZmo4{cm#fM+tD8Q*wyf&YN%ib=mdJ$_*8bjEErkcc z?WbH7p03$^&Wo9yPiG#ezqNFI%~S2nZ*OiM-I;o~li{TJz7MTec9*|@bZ!5ir}{4+ zb?fW(eV$wR_}J3JeAYVlKA-~b*SYe2!7|sgms>O0UA#VHX~x#8VcNTF>;C-sawEB4 z^!V2sk@p(>uP8Hl7<T*IOX6G|F>ST-TjBXXTtH(O-QKemA6%BbA3oo%Hf!Hk^LrJG z4=-HzBR}`aiHWT*{p){S28{}e94{l7QH(*uJS(dVtNqRt1>H@OT+7jPX1@LYUnT9| z!s9BRe!057&XiZ)ZqJT=OP8w7t$a4~<<95xw9h@Y&R(<8&FXQFanPOV+d&PNm-njQ zpXI-O_gL-6H=*INrn~j-iN5cD+P5mYoVA9}?#F{IpU+u~udjPHGyT%`eP7p#{C(xo z9a+R@^<u%JOZNYNp4W7eznWXgY?mln+WYk8Co%6$TeqF6Uwv=gzF)6iX2<`Ik}}B% zDBQK1*-p@>>*<kIx0l=heOb2u^1C1VqRtD67%Lq;;~|@QT&CDZ%DOD4@a6gXf1ke` zxBs`%Vw*FT+6X0rXGJ<3SnhYVdxN{q+^sj$W?!{2oW{xZWO{tv&Sy2%?1shuI+k1R zRlU9%Q}+FCdHVTPmXW0!U~L@><K$yKI$JJJY`6$2;6W+k(LHzhT9ew}poxIbbKi0O zaA9~RegB8s*)<-p!H7@mbV{WamvUU>k+a#cL+zMQ@~XVk(jPk0U1nr~T9c3U_I|lk zay_<O^my*uBj%rWp0Cx*-Su*rseO6*0rj}LdzH_ZGBUG?{QUvS|I7U5M#*g7dim=8 z@EtY(KF`;Wll_0zr9dm~Q{`&0hg)75#K`#1G<v!=@3S?*k_9|AJ54h!y)!aDEHZWK ztmVgwV$JUqID-ZeL520%{HyF6k6dqLmzz;`Be8wf&BRBuI9}I$o_*h^_}Q72`<ABJ zPvh!-;Q2*4zwo~5^q5IWW(&808x`|jB?QEj-%OppbjH_ZrLWgD`0pvd9$W4!|MP@< zm)?;#w)H;_%U@bw_jUD`U$57TKVMz0R8oA1Q+-L|;Wp9bhaX%C_K!{aHvb$;P1Jdb z&#zXmPc!B@y8Bj^A@e!Kc9}`rZe^`r^Zx4T>a`sW{wxcw#g^Y)w0i&VyYDaQ*Z(~I zXvf;Q>;COa>RiW>6w2PO<~L`ClFik7XME*<U1-0=$SxC5V#$;%xGnqryhlma1iSv6 zEG#vzVwP;Z9%p^D$X)P7#@!^fN$v7=7DtPMH>aIFHO-|<!RwB2y&<#b{e893_ov00 z*ZuinQu*nLYF&2iqrLBI^Q}IeP|o(#kJ_T~_v`w4UqO}5NMBI%4m5SP1yo9d#!obx z^seS!TH^Wgxc$GvuHRAH-|_r#dGPqlW&iavXR7`(&b_sz<h<?ol;qCH^rVw(7CN^t z1GRM4Zog;cnxUC5xUTDFR+8G%Yio@e=g-JhOMARBZkhHyQN|^pK~wG&*ZtQb)8~HK zwtcVe+fAqSG?|Pp)_vPNKh@ae>EB2}#`%x?to1&h`Ymw#ha0Gp(`nh3aIopi0cQS? z|8w4!fBi0c|FOIMuZy$3yIJTmdGy==tC(e5UG^=y<)K@j%*>r`8LM`=257FkPOv13 zWMQe<wP2FqJj;(qgg0OCOP9B={q^O`-uJcow*UWpmaPh5@dK3>TW;s=E`9Oqnz-u< z<$K$=q)yyr{6M%iTi@X7uJ>#E#r0xlfW{(pLazSWZt>3Nfcm@p_5c4~maBdvxLY=_ z{?B9i%ca+2!#DrA*~RfAXVXcwmrEx5MP==uzRe{%ygvTo!WmmN)AF-cA3ftMXmk2- z(AxE#s&|q!%?Vd*o}guXt0av-);wUAKf$E%_xJbsXWLyYo}ROQ?{ht-*mst7`8%Wg z`#+zvPG_9bx6issSg>!$w_DlMcl`-1S+twk?&G-`yf0TQ?(;It_H&OuGfBjFr%OO; z=Gx7S^Ji$j*l?Kdw9l&aPnoZ41-&|>?$vxgds8THqe$qzFqZ@B_iDdfbYGsk?WWp$ zx3guLjPw62TK8+;qn{6@yz`D)ubOUj`u^Q(W@$2AHz(KqeI38u@^eV^cMErdB_n9? zrCWO|Xc8AxgXwO+W7I2Ux+=|c`L?*fpkWeko4ZA)bwNW{xv`fX7B{o=`|W<euX>wE zuukIDu-WGMabhaJ&gR!m*1dc6n(5W&Ajj6%>m1#3+0Q!FasIYxM%kH}p6Bg;=j{4; zO#1TP@Aqck-*zQ8o!RbZb28_;`v2eS{iSm@92EN6yC6)bw)FMd?PA_*)^0kbrCq}| zf6x2m<9%nHGL+qVR&ZY46M8`X-TSWh+V8`2!lQGyep$$0tC73^?>Fzc8`J;&`3b5T zgMYovufM(U{q|o!3C_DYYz8glOkS$=MmTW-hue{V2O61!S#siPzg{(|{`Tg}m0<s+ zpi08K%B1GU2a}u|8{XXg*d!ibV|esa{KtD~*&A)Z(+!et5oy=fMBa=5?FUo}*t{s} zi}sxBQQ2#CUI_o4X_kAd=<I<jcau9LJ08uJG06xpYYwhyH(VX;4oZ?A@B7s37d+J^ zS@-C7@BXwub=Fz0tO)cX6(IAl+X$6n;OLhMa5%7>v3rp>QL2j=8Ry3wnubz;;>g?% z%v>BlOcDZ1I*C%Pr|@91_YI;}`erzQmj3H>=?M~Sw6SmqoM2_CDdHs5^M;tjtD*4V za)awdRYD;QURAoYp}}7-(Hk6$I06CY^#zQe_4gYzb`e~&05V8HT;Rh=)f_?-!(hWr z7Bn>YGmE)S2WceelqXC~b~{^i2{i*iq0z`DBJd&ez@isIM5&H(IG{d#gDN;+2qdKo z4NOdSyb&U?O2nuhP0UC!JDQl0($;8VMk=O8OJ<~EYS@>|8MhkFpP6YaJt3I5a{o$r zLa*ty74Mm?e{6}<oqFwH_U%1B2bTMasS{Jlc&RBo2;S5r#fKEcNcj+D=+|K<3ro$Z zQ2)h>L=^5CGaDNGPjY2h;4%$d#X-$~DkShhGu*bf4OtL}#sw)%Om?c;D;`4i;!p;u zFQ<4r99SOn)kF#@J0X?1pvn;L@y1D<96zS4S<pssSb;-8AwZ9jaemR&%Ne*#gB#1p z#4;Ml@X#0yWJKN>P05HXJX%~J%CXVnVzjtGloF7NOhCb5w73{8F0eIYMvDvN;=&~N z!19z*0~u@~h0#{yRlTr!=_RRE?pDOK`5YP?6cru>2izd0{i|@Hf{DqlH(c13sG*Pm z4vrrpA_V94!0ls`c?}KzkFUD)WB3PAhajXxn3(KXTM4c@0~a{Gpk)pfrWcio3cc2b z27kxIcCeceSresA4T~9v$DABLOyYdMbP%O_G?-xl$H2%m8q7$^dNeg7<+Ra416*oB zlE;uOn_d5wr=5?UQfFG|SM}{^Yv#Lb-LLfri);!L_Qz!|-Wuk`o@cJ{HGbvwBAbJ{ z5WnItMDJ=UJh<$7>$BLFCGTU*jrUsZSKS)2$yRV_XNUj#Q+~=a{IOg8trE{)4YF69 zulI||hKNa@0C`5n`M)gmK2BNvuEx~2DydG&YDxaBisq@8w3w^)_o?~+dQx8U`Qz&1 zs7uWKXZ(w`Zs(f6oX&S6Z|WX(qNc0B6Ev=_mY+{e*V_DD$Ln@}a3t62O*+?R2mc6( zo8kL(^P_E_wY=N^{>=Y;DXY0^xxTE)l5Q4a9Ph9|j)}?cYe3Fp$@o7nyCeJ3r^~X1 zPkWN(>F?XQbZ3;7cgfeNJuczfLc+hx=bLOQzt(^IU4`lE`#0P9vloB&BW5znq^qI9 zUp(~Dl=*(=(_Y=o{OxsM`KP|+YRC77*W51n>vdqorC$4cme#ND-*o4R|2X#&GZA%c z<5Dh;A39T)72D7GG}-IGa-pR+Kh~~Jy{+>gc;><7My>v3bJpJ6%G75raZ|ne|69Qi zE5E#dqwE&4jo>0d(2+BXxj2551g84^Jjs1CDCV)!gJ8~2Hnx?4s(zF8PlnzM`xhyA zT)OAY?61%L%lA(D`hJcwYt60~!XF|CyWAlbw2I=;jwjB3JJ$$(Sovr3;p*`9ESXW4 zj3?h;A)`C*fV$edpI`D;>r@7d&$0YJEqt?rzg>O+OU)wta<>D^Kb^O9Uw4%?|EmXK zyBHd)I5~b?@!oW`*l%A>lv(h(-`k>-3lF59Ius$e+T%w}+V_KN-@TZ1_;X&J>1+4Y zLkr&g&6nO8bZx%<|J1Oue{=fR-!;|$7D9|oyEr(0Txl&l**>S{<>#9x-`?BoCbpzf zjr&K4klkJ_&)GUJf`3{r`}FeJ<3Q!`bN8yR$H$%frT$=Z%G)_aG#X7Nfz!}q?&*GK zn{3|SEc1BEtNZfi$@TkZzFs-!<n%MI-o0J%Rhx0Xm%rM-(zI*)|J!BF)e|BnLKqsW zKu&kw*qg4y_hZYF-0hLZ+~0iW?OCuXcO^@8RqE}@-(uGG?tWGO>bCjbpwIULUw>aM zv;52T{eL$LeaQ6uUwdWO*ZP&}#LVJe0i6nyH)SjTuS2TmQ?(0ng&F62O-TDRg*9?P z+Q&&Tx-Yq_*T+>mi_8A~-n%*Lo^?^m|26iL)&3g4zW>9Y<41|$-s42r^GZqKL9qBS zRryU84=zu-_xIhxoBfm3&&SsU?o$h|^EQ`XzrXJPo3rKj{|1SDk^JBJ_0v3}CZWO8 zuHvFkmvk>rF@D}t`F+DFv!$BB&r*$_eR;LDqH~?j^<c~WPd*;HvikhymErFr^W)-Y zGOj3!j_qsbe?Q&!#mD2?-sPe5exKai7S0lNNs8Ibqd9Edmr#QHCs{ZIGBiL(U1=Tn z6!$;ZWc&VBRfO1?%5@w+QkGwy{w+GW)BL{m*RGQ1#?P0sa>vKY{5jcVJJlgzdrtN8 zt?c`?b9bIQ`10-HZ&8c#{gB!TNVOzt&9O!TWLuH_%j4(M4$fP9jHjQo=2G$G=hybU zJk8Ft(sEz*snnO>)Q>NpadY|oQ_Yta{9B#AKi&9mNbJqf<AO@FOw+5PwqzvUdMx|+ z^0LErAz#CG6H`HHgH8hKej?6Z6Lm@Kz0^eCo5o*v?hG~lc;r&d)XPWD*l6-b{5^5% zQjGtyi_&jq+%romSsE_*^yNLX;%{3@o;UCJI?SFw&u-UO{axRm-gASGkl<!qSk}<s zKU-(6{m-BBw!)nAw)_msPUWxux6|@wk6m}OZRLUIXWt$$EngY4U*ql1d&$A?<Mv+8 z=1%{!S~%7;-QVum(ev-GmuEzURQ~rZ|GVJRzkM%_-|tu7e}IUp(;<NsRL~06$$hAN zJ}o<Z+KVjD%UX={UzNRb%np-Z_Wn=e={LJ-@0G`t{ku3_?`&Un=-DU2=U=XR?0fxB z#kYIX@BTPG)zz%6d;UA?*Y%st*TwX_F2_~cR-c*G{A*e0udgrPDBmTxf}KO)g&^pF zwvcD%|9*IRYI5`?>uoDoYP33MiZ2UevfC&e{3GOC{nx|J+4pSzeUCo&a?AT~$J3t6 z&nxsStgMR;&%1B<X2qMm*WC{+ztlguY;FF5=kE2h=AGN|a_1Xac{yX1xxe!lXB~Ur zfSSUvrosiejEwWQ#ah*VE4HuE;(oKIw!3sw!uc;=2h`^`E?@ihfa317gR^SOE!3*N zJ73mco1^VHdzEC(u1`N?4~w6FQ2bZc{?xCJt^9XxXEwiLe^Ynx@Jspg=TH0b#_TNL z_55`7k{emXBtytii<|H4uY1StpZ0N%uX(&V_m4e4{;UpVoPTuVT=Ql-P2Py)>u+Lz z_gY)swzGJ9N$H-j;rxexk6)9~pU19Ie{NHL`kDXHXFt2G-)ixo_-%`P`g{Az?Y~%S zlx_$!&VMCl{Nsp^9Z{7r<VZ%H@9QHEEZ=nPs@m-J;WGu7tDc?lElcdn$!RaLlGm<f zbS^uccJ5~WokvSI$yNN@bZWQu#d#5zR&V<Gp{-oMJpR_MkJJ70Vn6@6m@Pjuw(RbF zF@4^$Q|$lmUCTXPsyFRfm+G!-ak)!Piahq4d|VlRnNOP6FT5t{@zbh#vk?OZSfdOS zt+tSraDQ*Ux1VTRxwt%p$!=<W+$^)-XZQH+`xG|s{>q%+>pvbm^;I`>f1!KY_q+CA z)hE8z6nk;2^6pXN*D3bb9>&*Kyn8Y0dH%+8f;WTL&lY%}>luG%uN~ujbFr;Ua?jNn z?6vs)wdBo?hcD-5^LxzS_i5&p-IJn!eLgvRd1+X5<xcm{zod0Dm)aRlzxZkUIlIN1 zzdyH)kC61f@7JUH>*Dq%smcvcbLYNv-eaG2Oa0$c!VR}lQGpLDwV%)Cw|?2~zl39Z z&W4-6rcUl&Y-Kpvf4xrQ#Yz7nIsbflciDRFwr2~jtS<K|e!ky)ch>$w`O|Vm_pkh4 zdnat?`*`CYDW3E7Z0;4^J^Frc-l6|#e}7zRe^*&-F!#pqzsl>EDA#SWc=M%x{w<wc zgDvq=pX0wTuUxP1wY;Wa?w6Hw-M0MxqOAS>+>FfzvNmOBOKxm)-h8uu-TwbiORfiR zzP$MI^W~=OUw!lC4Hf=A|Lwk{P?|sQbL7_ww|@`3JTL$Et1-O$@5h@j0jV!HYU=Bs z_$6>~>Due^Q|xv=?l38M>g9Ml<8gka-Q905v@<`S+L|tJU}XFG{Pj!!XMZ_=Dk9Fe ze!h+R*VnhLb8XhdTdmksa#Qd&Z}sU7=ic_8_1nMa`BD+@1-2g_Wk))^$j$z?`b%=c zuQPht>BeeXFZMpVvZgYBN%y}WPQQLnv3z-O<>yOxSi3K|P5qv~@7sZrC)MrC45xnF za{bZX=l>I<c75NuGc)Vx!gAF+!i{Uc=k4&aS|{}ERz<}BZ+YLPkw!eQXWk2rjEwX7 zoOEBO-rreORAt7bq1?74G;DQCg_h&o8ItPUKSG+UHmB{G#l|l=rL{)vO#B3~W#7{q z=lw0S(W$<=H&}~teukl<muiSn(t{gs4D91-K0JGWy6VrNJNvKN<gflJ#$K~-?uX55 z<m2ycwqu<Cxc+0wu7CSxM&5YLKfl)f+qrG$ME~Y*-F!BZe@W*0J@f4Lo{q}A^FRH) z-Ge6@uk9M_9bTw@5mVsY+Y-0bgPn0DQ_vC(jwXe!hJ@uD`lq@TIJAPEOx1H;)zI`s z&vv1(7sp;Up<mYjIXBO|dt1ADXZQ4z-yb@cm6xTJJ^TIU?b)|~p8ZcbT=ll(S@6rn z{zenjbPqWRN@eBPZtRPA?)|=I%D(!G#V`BPH$A*|E0a5oaoOa3b{e|I3A@aSYu+Dv z`0^6_J+IX~vlRapE<c$0IauCbf8W=@**}vGWi_XJJg?1uxGc1GQPerZ{}q~le|`)9 z%oDL$)W`Vglb6$H^LkjPFI<uKcHi^+{=ZXeg&CKo>8|3G-Sp5VG9Z23GRMUn+O9j) zjndCXJ-O6(JFRic->%K3)wc-l>vLGZ%E&lhY(vz{uP@gqoqDfdZ^-Z9!uF_pj-S+y z7fh9{bI#lS)#3l%Q(0e9ReB^l^IT*6`Q`aPpUQroUiqMPqvPz22Nz7)x6$Q1&&<#_ zhvVCIr?XdfebqPrf9_}S^!qh++hpf2pP%z<e$utP`}@qFUEcENV*e6;`=2_pTMpV( zdZ>q${oPQ0Y5TwTGi&ySzu6rTmA7wO;*3YG-38~`y1)B>w=vf`J1g|f&u7!0d32|R zRww-}PraIdZqMVbUp&L2)SXnr(_UxuT>o;!YSFH<w~q6lJ?yqI_qC2ya93_r%DK!> z6VAw<cQKu7{`p^D!I`i3qxbl-&Q^LLeA;E|{Pej025+}MoAdMAu@6zs@p5zh_Wqpu zB`s{D#;x<_vJ>{rwY`7io8|kPC8=RYHKIb^biQ)icwRpB;CzmEb1HvlXI^Qa|I9gU z?$+B*nRj!dPrse`X7A3Qn*Gny1X;RrBT_GX$!uG>{Dk(;BW5B8=cnBk`2Dk4=Y@#w zue|QmbkF#>ie-OH7fW4?e_v%AZ`bzv*X_9OB~!&-><|j5ynTo8S)aGKIDTv~eYsIn zc@x9&spb3T%{#62vt~kTpwf%Y2Np4J7Vs$3_g-A{<KT=hzq}(wr|HZw`h7s>#$@07 zk50#*fA-5pdeh52J65VTo5|Hw_2-7Kd2#Du^_8{tJ6X$4EvuWiZpYI_HNWeAUH!=a zV@~yX>6h-)?dIoy*R_u^;$M8Ed4<E~KNtCzODJizEwak${`skL*5BZk;zv6<uFl!^ z`GLs7DMh>gZPYO+`F7*Lg*4|hzxora9#4t(U-Q`fdjHx8;n%tO85^_P_f7t0`~T*i zLl>&L&rbO_sWto5T8<w_w(LxwW%RdpzUybjwWmJ2Y+Y1ZZ*9VTx@KDYOu0WNFFjrU z+2QiG<+d~ZyA@9+#i!3o>)KV>mg>0hY~BBgcQ*e2H801u^Q>HbX!GHh{!4GqtJ6Pg z6uZCV<h|}KoJy_*S<j8<`+ZM8+;-OHt;ox5&C^#^_vZfl;Uc!J%Kqqv6u}j<jJQpv z-d=q3LTq%()y_qiF8OJnpT_SVR(G$KV25;pDI??jYkPLte*f-YsnmVGX5O>tlz_OW zPahk~FT3uxXHI*L*zEdUUyh|jKRr0>?EXEz|D^bB>J!Umhb}$&uY1n<8`rA)r$;CJ z{-e5X&(lja)^*!{)*kIH`&~BM-gf3YNjt6edc{}E#aBn4SB(7de2)%;h~nhmeA1WH zRD#aLv=lFUWgfZfCA;^spJ_8!Rjhhv#-z*N`$J*%8)M&J*BO)BnY7a+?w9WQcJFxR z*T-$k4CU*mg)iRu#kyN;bzbrB&DwE3>8YZvi()rCSIYd#n!e&}FyD_kb9f(bO%40G zEYkYbEWwxk+xN_}7iFBkA}VBy@8-WX7Prr@{Ovn0&%WZM$@89IG5@P~k9~*~^y(@r z40Ba)W9=?`Ec||1?D;CI{r_*jWm}dzeZJ}b`#=0!O3ZA3Zk&>O{?hl&4=>iu2u;ws zm9)Rcz}0negxR%=YkE(#{p+e!>M^aiuDzCHNpL43L*q6kCc9kUs?<}Akp{|4)}Qrk zENcHebbt9(=g#M!>GPKz{OmJ*-|nY>Ud~t@c5eRvSJStjiT~gD{NJ?p`cE78ZqodG zW`b>e$d{APPo&F7KU^-|@7}j-d0y>l-aBUTr>C07&s+cFd2~<wf#*BT?R~P-7S719 zzpo>mmDT%e;j+B?w2)uF<{uAR)VazgJ~HcB&AnswT?OYZefFEa?%sMkm5_Pg7kJO} z7r*n{O|GW$RY&S+|D_*ekC#TK&a?ezwaw|m%li=qxm!~H@A^7je$SV4?wMDU<<F}B zP58U>a^vfM?qgG!?GD#%^s!S3tGxN>-_e@_5%>QmajaSKY4f2!$?ZG361PvY__MHP zTWt7j+rL@eKf7IT-Eo_^;$ozNW>m}KB#sRk?lGoHbIWe8&b*~lVQg}{Evf2X%3=H0 z^DT62Kc1RV)3JVYxqNtU#@+jT-u`>d)VA-_lkUnncyoDP#3g6KDPNtHrKT!qb91RE z<9ti88TNZYXU^$JFUvacCU`0HHpv;w&K(kSkJ(ogXPJI^tJ&G>pDq+nPpzC+C|dn3 zrY>j0;^yfYFIQfEAHOW`?`gg}TJis8MXx`({jqrTz1x?5Nu}HUi&ws!|Nm=ISIo(~ z_EM9cooZX6wS5K8=2IK{mxOOpTxoWW$+-SplkwRo!E-ljUPfNhwC2k)^R`=S({k;@ z;%VN&HG&zP>c`7v>vw&<TJq$o@~7)NKTlYCsbS{qaKTgQC;mk~ThV?wTE1q|wo_VP z=1d8Fr5PpoVP<Vx-OGRS*ZKE`o)O-@J?(J%+ZnuzMc?kR74ys1GfA$q*Aj7Ex+vp# zzO_N_cBjv;tbg*n{Uhu=^XP=M?(?>}R{yfOw-{f)adQ8!<K905;shV;e0e}M`}04= z)p}o6uQ?U4rQlw|$C;PDX`h)ECTrvTYCi8vB3j9|+6oUYZ=Uyd-P?xy_S4wYm%X^I zU;FE%dgi&vWq$TQZXKBMQ2i+EEC7?gCdtf#`}gQ*{aCW)WR&#N?fiWGURKH6UtAu} zJ1cH$Tz@C{f6T`t=AT1dW2Z;_-_*SAg#7=<(_Vi%zW<Ka>JOLCid{{<_UHb>mp|8Z zoquq1ncx0DuWEd+mmGV$e(CIaHRfMuCvEyNKWD}|iFK<tiawmfvDszC(!JmI|CgER zw?k3-lTP+q)|y?r`wr_>EB^iZ?)v8}^FY}ZGICklY-i7#!@KzFCHBmlKbLujzq4NV zDnRhVOnq*SyAxSkZ)HDy<@U7Ud@BFGso(Y|RP4KPf8SmYxfPlDvTuF$SH0izy=L>v zi}wDqc6s~l&t8gjkNq5E_4r%(XT?2TscNV1`E;K*%?;~aS#S5}hSO&MV4J6eM+YSM z85!q)JdmAwk=ajf8hg4@fbnOZ;APC)rKkMzx7WMy>9lwFN$poPsb{}_x@!03&eckV zj7>VGf{imbI?h?M$Y0+!uI$2%Q;yf~RVD9}ZO^~^x8eNV!|qGh|IK{!Av)I7s_662 zOYh=mEk66ER`EgcgZuNm#s5A(TeD*Ia=v)I*Ue_LBh3>}2haETU-|Iw7MCzp^|lRK zw|=nIoN_LY-SmA?=F`UH_IFy>?oYj~|M%C$FOP1^FMYXumd)AQo~nyJ{aW|$T~g+( zHJ3tm+Ro#>UwV=`e_6I%rM2zvW9?6ue>&w`opN~7--^eTpBcjMPL{*jvxGPJbvZeH zY&mlE@67$>pDyePIoA_8b(VaNwZ_?d`|D;bpC7-;>))TY>~%BupE!JY{ptMUY}Lp1 z=H=VQRe{6cep&WDS@)W^?+W#AJ=*K6S8F%7{JeCfK-#Xh0FTG3j@5js?CLT&%`-zU zc517KRCGMYk2xiePrtl=z5k54t`_fE!{omO$-TGtJTmNG5`O=;Mee36XI}bS^RKhD z-N!hegV(h}<5uTwexKVq*MAAb8h#1qVZ0U*yJFkKyL*IxS6|<}_);qW-l^;4zxl~| zb%)m0@7;Ns-+adM+1Xnq2zGA-n3(K@U(|lsJGpIH=JI(qwe`0TX3mRTp7(i`{r-xp zdy3BeTD^Xj{r4Bek*agol=!br@}KtR<=NxTY-?xlf5*H1{@idWt54bGTW=m)=Ogj? zV|r$*&Vx_)X82zQHEq-1>`G|wmYvSKw)By>bN1aIe^oCXD!#`5`ieM{UAWDkuv_2v zJp5S~GljQz>#YZpx9%>#@qTGvjRyaYH@m;Zyxg#+$@k(h^9P@=vCm(U+E--}f7dR4 zp7!4#+K;cr?r}@sZvXE$_sjq9s%(8{v#yu>{d801(z)lRWq$p+JWh79nf23;Uk<v@ zUpjO7TqWB-n^U7=TYnN<o!kf-4d+~J{{G*GD~T_wZ}WIDKcBVj<>^0<CYfGdKEH0x zwMgBzhFLncpFjPrRGq7T?3vcF7q7P7W_=!M$1iZV<T`WYi{w)!Kdx``vb&oxJzX?r zU-iEaH7D=CH_*u4ct%Hd-P4uVk1Z*!x7L~ee0QY6vkO5AYHHtF8~l0fw|@KR{Q1JU zWuUg?oy?ffdB^5jzr4SGNqqeGAiJMeWZ9Rm*Rw6oo%WIU$C0jUHhCFU@6PfE|JSi7 zey-@fRC3w8Mce*;np<!CFVE`fvhYuyf-F&mjT;YF&Yc|^_A4+fQS+AkroT6IZqNUA zYj<SC%f)LB1?|^VJKbz$7#|Y->&xbi-aa#Z^YeFXTGSPM#`2B$=`FkGCSQLNPq<=K zW@Mb-S5oz1r@H%|`C^g=W+@kQwl4qoIr`;M^Y&#+#pP$D&E5Hz%kb>9w<|N>@3ptO z`~QS-<^dh=CADT2o1<6X{kmrP<zD|ivzN`!`c(4Yf7&_iqb1uG`TEuA*nYb7!o>f- z=H?5hChS^eRIB(v_;x+4M6tP0^evBjX{p%Um1px@BQj^(+s#~b>0HofpV{l~<$v5& z`?9m){8aw^b3vV)KU>RVDz}xKnf7_PrrP;a)>CguzptNUW>FYXReMD}bJrSqxih~K zXX&?j{r++J{AY*ImX>F;D_7-6i+gWQIeg4?+wA!@`n8|`&pXF#cRw!q+?UU1*`92^ zGmZECR%f#_;al#8%~qD(dbH-}-(&S<R*zSfoDP1!^k>>kt-FOEf_nX{eL%xm@InV0 zV~c{qgUd`|`R9IJ_<Vi&=l4G%UtQ1dGATHallklAro=s)6nFhPZxZ`9^?q%BiCs-t zoR^x{@|5$>J15KSID2i%yxNCbO<w)wzkE$wep*;u!RcGC%f6n=eqTE$%<kL8J*Q4z zJAHOi-S)o=^mKo<N5}c@-<9TK_kQR7D2ugQKlE&U`pfIU^Qq<be(~p4{W!8Ac;fP# z`Ez$(cJuz?-<{&zwfDz0_m}6=-Ipe>lbz^RYq4$5S=MU3Nn!CD>{eVAW}M$}^I5<D zlInTY_U2L56K7t!mwk5TG)aTls+V*7KLwm!%D-nO`?91q=IispExyl{-Swvw7w$|u zwaT=%bl;X&3nVZ3T=tnLC;8KHmwEK}!t4D$i#I>Krle4ExzT&s&dom;xw6g=t^b?B z)t|w>#=QJjRv2r>#x+J)XZYKBf9I*UkDavl-_h^;XWHLiqZRzGrJms6Vxu1DZitzy zzNhbbc+vQhy}adGz3rQSc`<($ugm%&{qpK{_pc%Imf!uVv-!ZU%=WAOXWZCUujjfv zb?^VnYu7JRUN0wo<)?Sp)3m)uZ-wp5+%}J6*_vssZc59)Y<-&UtDa+CHb42^KA{ht zxjX;P41D=poY!CZzNB<sXJ)T1lc9S`<IMHzO`;zMuUyV#w?55r6|-KA-rZTDmtU^P z-TLOngwV~2TNbVF+xczj7tgSZ%QFHwHJEH%XB%?=nX~t|Q|40rw(nZY?5&DQZvES$ z9?bJFbc=+M&LRIm#T6Oxky&pF(s)_hx|ZK1JgF5cBJkm*(v_B3xkqOAFMS?YZxLH} zKY#P|KQ?PO9kO}3GX0)c{=UD7|0*uXoK6!>Q9J+6ok9JOh;6a;?;h?YqHOt-lK$S! zmHzu~qsg167c;ND{Jr!ypY)_(TPM7^VaDBaXKLud&W1Dfb$d$ej)dp9$8-HS@@<dp zugfJzLiLx;-u!TRmzJ5KEt8)18T(qRn$JHkJ-E5)mA7w{KWk0W)rp6HKGt2og!_H9 zvF+zyGyW8QoYZ~R)Yq!waOLG=-rw}qO!wAi9ASU?VYOSz)jGQqhb@o#81A|CP5a!m zZ&AhPYC6^?RsH<dx+d$Q{HE*sHLCxv_!6AwopyNg+wJFKGB+!3zQP+AHt#qQ{odE0 znT+kT78J`>y?a$zv%KES<dXHx0uRB*H?y8LU&`vvF?QU3xO=PqhiAX%FIgV{-+*^U z<tdZ=|0{3J-#C4KXT#aY>w7$dzt7g;om2YCM&;@$Z?}kyAJX#6%-w3|Jeym&s>Wj7 zif5Oniiy^{rM*8tF~5FF+6k?HIurKRv9u5|&h?yyrKalKCim+lCoHp5SgSTYIV%2G zJ9qo7#LTCy`)(~>pO}iR)Io0`)y6v<Sl<8bqGEll^7;qDhYR2CdG;`KX5;Ix7j?Hr z5@W(%&{~k$yB=?R*ZeR4tjNQ8cdz{mjmrAB^;oj67%^KqXUt$_sri-hUiQ=V(o5T~ z{hhz%N5j=$FT`%?5Uh(D7#cYh6&@HXPTBoY%D-=qzOK>vuDcPJl!$S7DClJ5p!{3n z#8`8RljBFoI`6jv1Y21S4GvRTSZY>Xnf<Mmn3<{~CMLV7uY;^z2-X+^3IVE&jPtd= zE~_PCs$XEmoQ4Mfr@OB35uT%9;Rx|`II!GnZ)QBff|QYoMN3`bLGVt!t<Iyt%rP3w z$VJM6(bSB{^Npj022#;FT4*2@t)peL0%FO~XrX~r^^X=B$SthVLSwYhKnj7;LIWuT zMhgw35Ev~qkV60|%Z#=(kh08ZO9Ls(jJ7n8v&?8qV`LQ?$E+V1AMbPzKY4hc&l0BF zJhgdBcZ+8GBIOU{)PV>aA04LpKab`AYtPnt@Hsy5`yyLig$I`}FS<tXKuiu3(3yr^ z6+X~4if6nCT2=x&Tqrt6Ovxb3;lOe0M#R_xcE<_4$Yo@le@|@NKQC^MA0fM{-^$=n z2rm?02s1I+)h?3GIdVix;eqkSZ*$(NA-ZPRT`1h&&~X09<?wA=__;WKd^za1(UZ_f z;DRJ3COgsB0lSS8Sy^h-)@%<axL0NJqK1a^OK!aWw(~e>th?t@7a|sr$DEMd*jUEG zQgf@(aHc~z=uFr}DRM{y5ZHAm)UdGB9CJUvY4V<ihVv@_#TW?ro~0_*;lT3ioqHXo zva-~yy0iNm!4qD;@G>&a-*=IJTauEfz=xS0*5}d)-oMr9;cy^b>6OmxTi;n&YJSDd zzoA6PGXbF0daJ&!_&aj}XsUn8?QTLtzm1+j4hPaFy(zQ48_dLHXRDfNODHBA9IBa^ z?B1T7;kSSrG)dxM#7!uTDHy~tG1)ynn*Pu;*5N>UP(AyNdP07=@SBOrZg2h@Q7s*X z2f+`|ZOta+um$}b96!EH?AstIyd0E1Z{}~CMJU7g_<#Z`IREA~JJ7)d&vzYRBN$|` zQN0T-A_5;~PPi4<P0*HEOiXr@)ArwfEhi-K;iaO-YQp39w?qX#WKKUO%wh$)KI_g2 z8A9G~IPim$<44c@`AgLc85!q${^3pdPG|+h0Tuy)4>K7~#4Xyz#AJ8%ZFDuktxmr{ zyHMj_a^5x!<l^|@Gv#jDVuH4~s46@#4lJGC`!*X?*sOKWA$ZT@3eX7@Uw>VZ^97yP z%GvRDDfaB!;NZ#vI<0%lF{$h&jLd9HPO8t}qg0)04p#0^%4+xf&1RFNBON8TbGI** z&foKK|E9%EcK=1o^rJ)>8RxJ1vg9wkEJR}nyFCEi_Yjr4^{U#L?n+Tel3j3|_1&J& z=SrT<O#k+y$~t;sgMYfyJoO8W4GsS67V~hP#qo+|QPFwOb$|Z9zg+eY4q?6kb|OP# zr?bMUcfN*4MSdQeSO4#)@SaKi4d;)1wA(nfHUu;|SJrbDm!Y)*50=~i&74*L@8{$K zfB82uvmvucyA%RiYT_y$ww7E8bT3WpoHyI`!1CuhYcqvFNtVU>t{#r!r!kLfPSvZG zntnWWe?A_+8`FFg>d61w4onI5n;HJ_cKZBUwHM1ce)t6B>4(7z99-#nuX@0o8Nwf4 z+9qd1YL&gZ0*v!Bs^9M|&)*+%_gDAhj$NI?4-TfxPtUE1OYalzmz#f7*L#6GsKjr% z=moJIqkK7_q6jv3rpm6{exNyT2Uk{>ny5DsjPu0~p1G$DDmo5jEya;#7EA(7xxERq zb_G=(lV>zE_;<_dJ-Dni<7PGJ(CYArwNr3p>5w3Y1Is7f%~}p}Sb~tSz=zC@hgoZ) zSd6RJgGwK+)l)&v!J@m-i;Lq&$foGGA|T)RfT}~?vK<Zn-3e*$K?yM~acdAHfq@)_ z;_?7bM#lMGWvl0cl0l=VkHdlGNBP7*WD1^nvsX#s!Da0=VYr$STAB(Ef=}jc4F(x# z&<R@mcH<$Foo>_2y`UPhs`aWDj@;{@%F0rsb#rZ57sxYehM;W~)-eZ`yBci%4k`lH zZ-~+a`2<T%ykbd1gTLzTtKePj2UOG)9$Zdp=l-#UBkfz9pumThqG8&&=7pz<2z*$X zvi+7OC@5weVF5*I!AOeKmlA9KU%9;GXZox8&t@hbGr1fRygdA#{8YDn|KqCO&E0-k zeSYn=kB4Gw%DPUfd2@f6Z@JedD@5w=$>XsxtD-jh`Y_H1-I#qdZMN=N!{aiUt5z=a zTEFjC*1!LtdnFE9r9Piq?x($ON7CD`a<yMBmK>8zUvl35{~oimxX$kh2n7|B-`4zv zq(0C*@zHzlx*6wdZ@a7db$-<=vF}TMzNxQxFt6m@Vt%vwO><WY?zmZWQT6h-Zhp^f zeFKZHvui$?74I|r)m-vY*!gn2e5HczuV1Tk=e^H4lyZ}&T7SRSH!JTYikr_*U3St* zd4Af%p5^zdl<$6fRQz(q?SFIc{eE%yT-fD@pO3HE92k{+fa#}=eBH0iTTi>XFP)Bb zSgF5lpWKJYoQ+2>wMpmAcs95Ep2_Pqo0py3^xb2p>xO4<x8L{E-~Xowd`NsV|F&=Q zwAZiR{QI>2{u=OIDzkN&+(4Iq{Cd#LpLO|;;suR@1B~oT{{8)ZU2Q(-66_}@CT>0c z)cRw0$<wLfe((2u_LH(IS@9r3^7vml@s0ye{H)(jNt>BE?bfb#Ug>FiyIw51m0S)g zAkcEM371bt!+FqE;GiofK$lqEI(@u+xw~9t%B(#ncP_r=^A4JuVnNLWq02jF{)#%F ze#k`sZGFb7e*d1^_rl)Gmfr4td8GRNi%&_->GM?Q?)o?_^XSg!Z!a0mPd(UoKlSzh zuXD|p_wSClnEq^y)`gSOx%1583MQ7tKc2b$zR&LXQctOCy&nSBl$>^*=23sI<n^sz z{QJMQm3*o#zb5y6t97mE17pzL>sP~~mmU?5-=n%|`MfHv*qV<=U(QUQmzlZqT-v<M zWiv17*M00ZIm~1HWOJXKt<~T2|Nr>Q>AcE`Jnw&t>7$({y9W4<(dYB)_wk-8x$HZ= z&+63*lfPdsKR<u8`ltE+pT4ty`Y2owW?H>wlUG+)%4Hv8-@C8=eV+e+$s_JryVPya z>S9p&^`X__<KK(N<-fl>!SR0I@4UK;Zy)#D$8GNCz6QGQ=bGGZ=owoj{tgG0yKdhn z_F-oBFUd>F)phHS?#-zG7RF>3Kd(OE*Z#BHy)UO;)sD*jzV}bit!I;pFR{m3OxW`# z+j_a_GwF!x&h-0r+IN3{>e%w&%;sfJpUqNtWnGx@zV1h2)XO!&m!gy1u0MNwYOaLe zn`Lvq`)u|x&b9mfR(YO(bbO6pmqAc`ZmsSE;gi2(%Wi^i+<xp@_W7*&^5S!r>3gmP z##Q|P`yF&$_(^rws|zOk+gaLvI-&eD@{an2kOiw5=dTFa`gYsxYil3hNbZ08aYNKy z?h1pGoGuH}L01p0Ub`*oa;^A>%u`897{#xHXMz)J%_G&>t&n?j4)a-0*>)lpRF*0@ zECA(Wv7^7D4y2!uDPAgFerxeIarsnd@7fQZ-!EU^zw`67FUxagtUGt>(dM%g*WJpy zZW4U>LPCDMg<9;VAB$g}G{5grSies)-D~wZ>-wnr3r@MKcw+y*3I1siVR$91I(Kf) ziS^yv?rr?`MgIMM>%U)~)nAj#-}GQn<<q$<m(5U`UltTr`n+S!FTe0;mu9^a*Y@Xq zv$~ixWBu0iKZ~m#-Yx%hy!Pw-eIg$^A8!A4D|`KxLtNTh9{zeV**_@6r1J0A>zAeT z_e|uEHTnH!^YS0{2U*2^f_+UxXQwWjbiVX@?De#&)xw|)N&d7{Y>e?=yFFt5?JVvR z(+8UmvPmy_yY2QmKILh(-)^SAUgq1uUVO&Tz3buZE;0M!^?ScXeJq%?qxbw`38f8p z9(gU~_~E-I_re0lOwcW47x%w4Wt{&l#Rqc>n+A(o$^rGb&vVP~d4gIrRi93(fA;O* zZe9GVvZj1Jv?FmB)JV9vc0Fs&HQ5~ry9!npc0Rv<uQuB1SKsWYJFBmy^>^8Bf43=o zR{gPmrQHAi*_SU%e!oMtH&4hQDrMTuOD}H<M0lUEOTTNRey6zl*VVWG*Ub*AD|mao z++VxK@Xz(dH$OzbPI)-{*tc%e>)yZDT5MZ&aE4C6rb9FL`>(J4Q1>{;SN{FZH(oDi zl$x!w-TuEce97l_+nDM*;qSjJZ8(1`d;Q!+d(w6uwb*TbuOj(hz048wY~ynl$+s#V z_Xg|4^Zlxs#(THylBYW8LOrwSHQtw>&B|W)<?{T0TejWX{ce}`+`?m$u~FB2Se!tq zS7OoY4~O~v4P|1sa7>n||MT(ZU(UbhukZV|b<0J!$9CtHG{g^X^IUl}-Ls}{N_5Dl zUoRH-pPH=J^;<Bk>--7bZ8wyBKe~2oz42lDy{grD9F~mposMB%Ip9#uBy9CS`1Z$d zxAT`T?zgkr`~TmKVza=Nzd%=(&Z>Mi^Y(3@%b=#s0&b?liQhrjD~0=Lr`=EPx4qS4 z6=?H(n-vq2o$j~Pc<vuHwVg{ZpOnsxyWQ>lzc$zE@4??${nd}pDXv~Sl`pPz?XFPy zZ|`4UnlHa+dtTJz=9kOoXstiC`I8NEmcQ%Kg;&r0bH3bVDX3DFcfUFN^S15z+-Hrl zcNJfMf92WjuU-3>X3wi!^lisp>+W+_)xWOy-S@iu<@NOQMNfnKm*&TndRA}#zVc<Q z>I37_u!AqJ@BdfY)%<CZ>kf4$zVo)<V+>awzTL>ozO0$gYK7Rb&YAW<9yFKSNNisk z5x!d2ujN1JYWt1%G7h)%`yZDrk9qaBur>Ow-{<eSjqBKIlJtM2&Q}E0_I0kO>NiLA zH29ygcV8v&p;O;t-`BPITUWY?%<p|Zw><9O%-x^QSzoW~V5!Lxfz?8wG8Z}+vPJpB zeU2YnHfsu(6oq`>_jcRuS8>HNQ>U$r-6H9K<a-|IlBZqY?^R#kbXsrrwfOqKyPm%m z`_|yV%913pw_UDkMV>$C5<-!w+g4oz-BT2$y79*L%4ajxb+=q_ntJ1`M!|;rb-$Nt z%oET(cY9v(Im^rs4>l@a4@pvg>mtp_INwY~*8fE|<NOsd!lF4{+kQ_KU%q-?-JXgY zkGB{5+dJ>d-d-EjlHc|1yL$Ul+x7dlRNQ#@_K)#b|Nr+g&+l5eSFbiH<JRk0r*G+q zU#-i#S9$){7vA&s#<m|$ow)V*)7<6ziW6Shf4womljotRb7kq3WoPOf|0=KM+j3@3 z@$=1lXNCTWe7%18QgiqE3m21WpY7Zy^dYkH>D12)lTR6_&na+{GRujm3|jtudVJl_ zJt65KYc!(fEUAAo(Y>oZc3*FUf1t0bSeG`l+Ob!%vG)><zjZliGtQ5@*^sv;<iK(% z#`!-EKiYl&Z{DoBUoT(ZW9_&77Gd>z&E}wx_dh>P-#>??=2Zoj`tXIzgZ+XZD&Oz@ z{)t=p^lUM8wZ&ZjTe9Y-DBG{u^=ehA{`cqh|1FPJb}l;FdPY2^!11F&?NjY|&;7ry zuFvYfs~BL~$Sz-Fu>ZuCJ@Mst3f=D>{qgj<+LgZ+%f)XSCLQVc^Pk`TkHJ~%_j@uA zG%#lFd^*kh^I7xw)3b}-Y&^b<TYt}nZ80H&x3gBS&FVj04!XpC{^Oc27u_#!K5sXh zJ#|`4(Mgg2=M4_?JdL#8E8ofvy4teF7Idj}+?9lnN5%cUbvC+0rA&1FWZ<`4`{f}X z(7k+ie-aH}Cw1#iJ7@R%&7VhKlA`?Y?)&rUw5DBfx7gyCGghzH96ijJuk0cDy#Mik ziCu<zC7@1o@C+VFqa}w^cmGadv01F(`X&93yzVcfIDIbHDd)Y*znaEXzukK4*P^bV zoc}@Ix>LnsOD=}6<nP+G?%lcYJ$^jRmWt7ZGKPxr#&Ox3)+pP{&li82%{c$?^(gzd zyASPPp1eM$AZ*wBr_+z6i(AKR+gNpex#aUr%jZvttE>-cKRtDR&nx?l^A^=kTbumS z{=c5<iDh^8ZF->e^1s>rb!M3}-;}!bRjvQFqvrXy*zbOZJP}(~sECNRT4mY)yL9U% z`|34;vr~`nkDu_*^3Tg#kN4FrU(ZtWh^2T=hF+9P9lu>g%+;kyRpH<}sIISicjOP~ zPN-dPHf=k&e%+eWHY~@&9S)?|u>Q9`9iG^+P9jbBP?T%8Jrn32xIbKr9tpS|Sbm`p z^QxH(p^cA@+-X@;_J8vHKPrD8%l}_-`-p_#1JL!xv&wE|zFzkITJt)Vnti{nu3t7$ z`FL!C-QynPXZMeXZk*+rZyEjmi0XywOm>26EWh6={>=5QaNWkp?)k@i&F@*%{=R*G zo%Q@1Irlzo0hQ_e+@hBs{dqj6_*|sTnf%2}=fL;LrbO59$Inx2QUKl5SXY=AZYREI zlOId>E$O!^=TiTCXt#f}@!^&}n@=Zp6|dU;eqZ&i`u~4V?>310u3RDNctU&qo=thS z(s?@)yFO>iZrxz}@rdx}f4W+{rswuw^3r~KxYw0+>%1&~`@bc5e-3f$o5ZHJ98GeW zwJxl<>p<3=)BFB@yFGiIe%<oFpQi8kk<QuRc=>sC(V_<b?pd|E4~kD6i(c|pOn!P+ z^t+zPmpi`Cs{gQc$7<j7{mR^zbpE(bP)qv}F4lEZk^A_TOKjifTi)IMdtYhypC8L! zU(V#e6KtNleCae_^}G#{xwjSs_j$js{}cGO@TXtM<H*8ym7hJQ+x*M8^;^DP-#)gy zern}&@9Fuwy=Oh@&N585{WHJh1MgS%8KvA`4zky(J`fI7TQnv0;<5A@GtQddw>j!q za&cbOtChQsWE$Spg)M=u{%5*M>_aE>dE;{y%6q>A)fR*wSpNLJ^)(N)eu%(|T?bnD ze)LEhtJz<)tori8Y)hc5`l=^B9oMEd_=kM4{CXugd-mqXeb(S==i83u?~a`OP=2p+ z`L<man(~dTy$4#vUU)Ok=lK3+<8e97bKGT1T|GW*x$JjZdt-@NJxdPgX6{=#ftKo8 z_ohULOzSjwbk_WS%)C2a{zYZ4o!V#j>xIbF>|5EriL<{w>wZ}nXX2!6xaHc9EgX4n zrqf?s&k%I#I{!yBEF!TcY_`7i^Sb(v-SJy2{RREY#6EPM-|ftAYjU@?{J8S_BfFGb zO0NXEe?9TO?ZxRAmtWlOi~5qh=h?07^>;7kR0h0exBua|yK!yO!yD}!3*K(M9(Q*Y zU&xx}o1+=$pKFnt_dc6(e)qO}hHEz;xKa3Jk;%n|$^X=A{~Wcb=ACVKkAM2S<aH&7 z{i?65Rd2i2rMN9BH#~QHcGm*mi|-_VCf)jSXmfY_&)hfRZ{jb8*?gYz<<IVXasJnN z$_63_7iOs6El;<4bWQrzJbgJI`==o<m*w18pLB2e{VL<N)$d+reo>bf-|Qo@z1H-> z=L5|AS7ceXH(8$6+5ANHOjqio1;5W(zt@o~tCBuk<hsfD*wi=o_t*bBZ7Q^M;+Nhj zX;Thn>wmrd^3?trikIdDZp!Vme0Ji0|Mkk>Z?|7wy?)=UMJmyI{O-K>>Mr?kh+98q zwke;{q9+L#m0s>VrM3FWQ?|u>Vp+r&eTkj(qW|Zjn%K=Eg&S+qw;TRBf5}^aZocyy zcbmS2^^Z&+{jcaf7T}ovI;Y0&l$zW8BNfXo+Hd5aSkQZ2^VA356%Q{z>gv8R&EwIr zf5*1D+}{hj{dnK+ciz36_tvr1ESi66X-sl`(wt}OwpbK&*lFDJ-+b0A`knCmg+J_n z?O6OhSVvw}vwPd4F70bm4SN*dCEY)j`{KgFpLNrho9?zZ+<SfTn}=^UpP%>MjpyR` z-u#-ydDH*>y1svzzy04WX1aFlyNv$C%P<`Ot@=yj*D{G;`qTR7e_ymEt-AB`E$(d< z>RLaJ$je-hEsx#x=hNxer((rc3w_X3xq~@ER}z}w#9p&*@3&ijj+AbG+dcUSs8-*+ zys9QDoXPIvBGBc8yPnUh{`5X3dB@auJa;eMJhp!Oy{b~i`J9p^uX1kN*k^KnncCoA zvtRBgixjg$VVvf<iAODG{IJ{q(fY4NzS#Z0<z{#8S3aLRz0dmHj;QBmt8;`KdpI_l zY_ljAe$FHn-IAu1aAnQkxB2znpuvdQ%ax{xJYP9$ZGNxmH4o5Dl(%#)ziP247koao zOUyZ;bgPx`MxUQ%PR@Z>HRjy5=D!qpK>ghG+P|;k{m<L~uc_Sn^Y4rO7U4H4=53DO zUkAFX@YL^pXPsi_2)tif;cuE(JSVvO{8x+em^?$>Z^rlX&qlRON%Gi!toTf!oz^+? zeLtVg&K2waex-ch=ecH=tFxLK{JSTCy0wW@Rnz68-QM(GlwWf6|CMTXm)|MQjj|JY zIm5P;Z!YAYZu9oYmxs$AAG@VJ@AaFq>x!qB|BtPod@INB%Jl>H|1ZmnZT@wB(z~0r z^KaSAjx8zM^?Rv!cK_+}dH>%X%)C_>{A@Dcu7|U}OmBa$Zy&q6-YHBl<M|q+fGz%; z;-%uxUb{KldDhjq9qsSI!Mz}N)5#rmJqKJ=45jTSR9Ad|9VWh?CCOqohj~fK0omIk zkIRm2h=1BrbNAG(N`voril58hd#d2RIr8yz*C}bcA9ZP4@dmEh_v=-q<^CjFLl*xj zTD|dcg+=!N4s;55Rln_2pLfEW_1KTTwOf9qZhkr~dYOLhOYh32?Tb_2@Bja==FY<( zs<*wmncIFWl}%jH7u7RYYN^0tne6<jt-a05D(1WW+O~L8r{T0$me1z|bN?zaeIU#} zGq7lVhG4?0H>dhMl8(z42|npy?)nzBXFlkD_&<NIR6Gzk?b4tXbb0L-jb;hO`QL6G z`4QJ2Ql+i=MdG!YSMs*&kBU`}J-Ju)dhKV!gxj<AmmNQU|EPHU9W{@u7meoHiqB8| zD0%hbRW2sGxtdk;K*P5vgHa0tL5V%-$ISZwzwh_Xo@n3q{^0uk|9;(aE%i-*wXidm z_s5qDr%QK8G0x|!w6SDl{pEE)ok`RB%?9VY^V~C@bSU?&uy((uWAXX#`+9rO)t{xO z&FpUE@BjO$xcTT8`@b*!fBro3sIcXiRI^0(x}9lOZ#EnbnxkL&bZYpg&z$CWOM*9g z)ctO^|D(8e&!<zRmmfL(n_G5E)Aj47MMo`{%>Vagd1Zylxj<KmC)@Y`&E56u)#~oZ zPq#1o+wXnX^lq2>f9^LA=l8+R0^omi$Gs)xRO7<5*%t#3JYUqUmt|ZN9<xAJ^wJ?S zoxGh-r`60WGHlrK;@6tZ=dwN*eYAdjh+F^6^xHX`&t6)$``xVh!MSq^K|TG#y3HAd z>r7W^t=)Xi>gSS*vnytW)Sb2XFFp0ntgS2FUHcKU$8FE|v>AzQHZoNw6x)}G$IDDl z>{!GV9$OkJ_bO}KxsP+z9$a3O5YPRC=kGVh%)Xb)&VPS&@p`=P>p7Rdoi+a|^7qo5 zo6?zGC)LhtKd70;D!c2l->r<#TMi3(Ul-eJA<gyLT9&6ZboR!Rb0e3}G_&3Jo&VLm zn-BIFUus^zFJ`t`>Nm&lhwAKC@h&a>b9ejF<aaCnGxW~fZ<5fMb7F0wsPwkoU-o=? z|9t-1waa&GKH1d0Y_s|OZ51>6cE7$>@@DSb>CvloFSNe{Wt=~sy7)I1X*X#-&D>jR zueIsO>vEBn%$m0v$ICvQRDXW8Olp4iH;Yq`kLq{z9a8K)eN?}7^LaaK|KH|z;Wl|< zA9lXocKef|ns`jX!K7_``HvM8<Cbsq@~-AS8p<x%ma+6od`rQ<f8X~numAfx{_War zc~vc$i?(FBC$8Sbx`{ndS7Y6^46&rlIoqcm4^-9kdgjWpMqAg(ZK6xxsldIWA3D7Z zK6cwpx*VH(k-hN3%^3mjUpAaS@+<EB$p-&FIY$oundGf^^Xj{k&%0eJWD2Hfvu@qe zB$h4)o)SUowL(Wze-`K-ybvuOJ+1Sx(+P`@(R;#px`oB(Pg_0zxaa#FC)O>G3OUx% zExlOi`vgVjphBiC+KlseJXjZVRdH3@krm8mqiWbC#GfDiek+jIUhh$lmQvxt?3(o3 zfv@upsP9wm>#lR}lbIP_%YV9Jtz$FW)2qj4?@h}|ZxKGVc28~L!Lk<VN7X+q)CH$r zdc340JZg>gsz=9<PTA<Y@&D)Z_VxR0*UqWe(7Nf~+W*7OUQaGAp^-JpN<R4By&KCv zO|S_xRQ;zP6>~;LIq-;O^HH&AoujOWVsi@xD|@O0lr3HEyE`auGhg+vyyTyTo1g|e zr_}b96@O=_Khxb_R9|{!e&M<o3)|&R-IxCOufNM6Fm}n}YjYd?bCSPQ?)m@NH2MGI z|BHUry05=}ueeun!iGH`k4b-i;PTk0cWRl^hL8LI|K8ub`KsLU^S)<-e;oh)yG=T8 z1$XO_pRJ#s2i1guF7>W_x**AQ<NEu*u5JIM;BKgUuHWs%@;m1&9`}6tegFSHGtD}& zB#R9d{^r|`F{H%&kbd5{>-M9)v43oiop%h8moi9j__z7)#nxwU_r^W`t$a=J`8K_s zPbU4@8>VwT?tr@8@5MJ9ej01=D4Sflp>#(?wB*#=Q`+m#xU*|6bCuBG@;map@*?O` z+T~xb%ZJZUS>~>~tYZF@=$508Z1Q)y9&D93wEB+Hk;@S;6zlYlZ2D-uHe+MF{r{im zLCv<7-Aa2x{{6X`KEL+X9Ow1X88&hv`|m&cYpob9C^B!tddc;sk>42a>@7Jh9(`*% zlig&0v&gE<|IXQrGwONkt=@k4&izR@wq~2<B|dMxxn{8i8%-*n6|yTDid@}rJ>L89 zy$apC|4(_pyyGta=F@~Y^R&aoyeHdUYTe4;eBkWG@-4h~M9!C&S=Zce4&s>^y5)Ur zeX3Rd3+JPU|9tC@ziE`a`9{#szY`bV^IrY$DSvL;<zJW8qL;1}m#sD`J5v3AS?Ir_ ztY0fkWW04`6jwy=`}o%6_Lk(;sdxULI+5eCVgHlRnom>xQ~rOM+Tbtq!0pw>9}1ku zR#*vfp7MCs|GYoCeeWhvA1S%W%(|e9|CYz}V~k;&nYa8%mE*kmBlUV<bne!xm(2J7 zJZrZ3NZnD!`kH{_jS1E31s@k2JD9uqn$81ZcD|48?mLg}JEFgaqs4^#?2*Q!=j*<0 z_B(7C_)w*8rRnFczn#o4f3U{d-O$Z<583`Q=g-9zw}k5#m~4xv|KgsL{lzn8?!_CA z`qzKZ`u{0=&hn|p1(*E1*k3p0|HT_QJ0&ig^1VyiSJa)`%X~=U{nkJ1F;l#9!`wDZ z<+3X@5NCHNJP@B(ctNh7=O|;ulG?*7J~rsb99kZ;&yerkX?BIe195fzNA?|Ytl#On zL$}R6J4g6)wXIX#QOEE%^^fKC5(UIuyR&?sPMUM{%ZvFQf6TdW9JP9sZh5GsFd?Q) zZ(6@g#bd_Wk1_uDxqf`vvf?XrG#632tkMsdGeg_SeDBWVg%>mCgy=@idC~KJ$7$#5 zN6P;kH52_@`rK@l;gPD->XRN@|F%e!yLw%@MnH7;>9rqS@@l?IP2JVK?I4@<mX%C$ z_b<oS{e1efwNCIu#<fS)|9|9F?tD6JwQcP|R`DxN9KH9iAN&6GXnAj->60Ii``7D= zmp^i;tudFseJv-YDZ)<gPS5*)rxPbcm>trXoxiX0&f2XUMn@_qS}Yf6T=?nxvWFt~ zR~~2G^LDGnb|J-<9Rb^J{?=%6FFaIvJ9m5TXWQJ$xrOT#f9ag_Hre)Kcg21e)hTJu zjisLJ+xK;S7v#~1+Hr(+ZlTeC{raEYalK1!JdY^f!~R(2`kT$?<8&@PGkex=*Tdjs z$r5%(;<j|{zT0`bP47PEFm;Jol<{`%m;JBT?f%4lp!?^e+CaW}TdvPH6p5?3A1l|X z=;o_2|Gvhx$bGpy1xJbvx!g8$zj|yF8Y5DBxAgi}$!{9dy1PFf-nThWHtF6mG5hX7 z#ibb^?fL?ZW^JuF@1i<?QE5o2>a))7SNp&3eJ{FsdFl1o?eFe5#DvKH4a@tuSx|hF z`L-W7?f-l@{95fe>yN_3+KZ2#tH0?Md)@ifH2bWUc<J&kf0lDe){km0yh%4YXX(0h z=8wuO)fd5a^+$dlz486U?27g79_um;gIs?W@11ckqEORL`qA?ZH-Br}iObi1xtMhE zPg#IioDTc(MN)prE7a`nKQi8T=iB}I|20ddCPlUIC*OQjylsZu_K!Cs?b>hWtk|++ zgLNB^_VyotuT{*`pO^pV&&!PYr(eH5_xI7BJq`ZiUw;K1NdKm@#YkuMgWxU2n>L(V z_<x_--;}SVm*$;bH|1CA{yA3Jhc~UV|DAqtR?*Wr-=<YQ+}*w;^H;C6q5S&NU(MgQ znpZEs{Qh3<{9PX|c7DG%v2WMgW4oS;%}Tvia$B$Zx|7fLy+2|=L!p;e%kP=bf3oy} zR`!zj>-SXU&XoW6dRECd<;SP;ME&P%KKd<l@j>qA{K`+Z+}oMHWcj_S#T8YHcE8nT zoZoP};&Jb1#ide$?fmld{%`8%f67tzC8c}H+dy~O(C?=;>I#pxXN6qS;468|Y|VE@ z$x+JLWNP#h&BxRI3Jt>R^xhqB^k^|Wl{keX>4^S<6(55Bxk@U|_cgz3;_o`0-{NtD z_CEuS^U-_!k`Ji!r6@0y)8#v&q#~;MV$mw!7JfE<xjECddICIm9^H^oSa2(@JO0+g zt26fTum15^T}SDSknTF4j^<k)3#J{m`*_@2=76if(#Y;DzZ==*BEE<G`6&4E3C9$X z_l+F!NAy7>*?O)ER^EAZK>gA!+l!A5*nV4l=TWU-J;zZ-|A{UAwLK2*G8{)l*)ATk z-*cMX+E(yG=a)R5y<uB8-X4*6ny8UiBd@pT!=cJs+wWWtyr;iyap9c{NwtO{u`)iQ zx_sMC*H00<ES<mlOI&~R;bYBfEf#1C{p$T4S10qn(PDx2_8-aSbz+Ze<5dnn-YXcs z`3PfSa_I_>=f|fC?Y+xVQx$#V7iba#WdP@@u*M;iv-{S+d%5>+`Tg2p7O^fd#!ikO zE)`3D=$Y*E-v4Y?wiQpf$m#9n(hnPUyjT~J{<x-Fe_w^1T!--Qr=a>jM5gQQ+cif! zum9-VB(YC@pYs2+`E{3XCjI=`djIdc@~uA?CArp|x3#gpekrnB_#tS9pzdRaVg6<Z zkC0dWiMhA9HI8m7RNDWs`qB)!=h}R>UoMD<2b5h?`XKmz`y-VS!}=QkYS1Y0X}#TR zW?Xrf+p=Vlllx=WU2pX!*j}t?vi&nHBB`_Ff+PFZ-+Qa(ICpi|N$c<HyWV*rg2iN) zQI}IlYybQA()#z_o^9}N=}Y2Rb*z}pR`Nq<Eq`-S!@5ZMBj2B~%hwc~4&C`kCN!oZ zddrWP4lV1apM#GU_dc8?aZ5XH^Ioy{^MAb2j7q+`;m1tPcpjPO>kqAy(b1gNTU;vm zN4x#Yqphb8ce}c7`qcU;aH;w^Ws~f}eGk3ke+9+fKiVNuD7SL8u9L;Z|FJs@>Sm@+ zJJ~)nYmdXOz1O=+-78e=PAAXqDxTk4dm-+Cdfl@{LctdgDcbD1y({Wq`rI{1!8>oA zJ7@j=&a*qI8+ijcl+Ilg)%~@1&soF08!rXT;kNyLr?}hoRi5$=iQ0&~kN>(q*V^9g zIv>V3U+>hnr48qWHlLpfo~4-fUyu8Q&Q#y%`U_55Gh=st_6#e0`sDXJpZz;uPcxgE z-}Q9)DZ49fDTh<1-Zo#qOC|Q_mE@<Mx_9>{-HyGK#e8gDKtJpGniuvjCrJOlm?rDK zesAKhZ$)1pOYHmb&N}m*WSWm*Px!^8j!nFq6ZhGj`?mZ}ndGJztGd6w*^`f%&j^XW zSpU%vbc9EHmCTi`)6ewA2Z4q{s@`lo{)u@%$K;ZyDb?<KZs+f>jlH?mVnavs4~vA{ z^+y>~|2*n!?pfq2W#*)JHY!;{*(>*${;yB^ZO5J}-IZiN{#1<pN$<(a75fYy1nUT& zyw34@%Xe-|yDiFvadtO)?BkSn-qLw1_d0jY8e#To!9N=6o!!^<^OWxh?cZ@q^GZbh z9>2%(-hxZIK5Ct2Unf}9k&?aHrFxH>j&Qxol>Y%8-@RlS#2?O1ijLab`#q_rKg#dP zF?q*{O#K%EH;Wydu;QAC=aaOx>P&VY+r29@*8NzlxNcI$`lIrC8|9xe-{e?&be~D~ zUEUudFZbTN+u;AjJ0>vXUrmR$t<ufAKaK|`%G)b{oY%?xc#-QZ>uZ<q86B;8T&?}U zcwf^rj5%VTDkfpA2g2$}j}JX~WWDJ}pQYZpUa_l2i?ZA<K2rJ6XZ<e1Cg<h0n*lm! z!vsXlr1me)H+^tff~Dhn=P7|eN0n9Uj@yZfUp!X)ac*8rgXI12GV=$QC3rf&9{bt2 zBf#QZt4W=|{Bwt-js=TMxqp~!)_A6$m;85nTvg`X6Ra1fU-TFJl$o>9_pDR$*+b%i zf0tLti@olApZ56CGmG<=EBe%#5<lLp36D-bpziaC>B+fso=1=FH~bPHn4}T6-f%^j zPyZ_0qpoL<ZrRa0!GF$u&GpMS%2#h{@(;1hEZetWIg_1WR%iFYZtjc8^2!QA>!sNW z4NGq-&g=eOy;<w$#+=P!@7Jy{5PmMuEvDGJVogX)i|qTa9HCRRV~(B=oAW$tO-$$- z<-FwEj}N_E6kGfC>d*7JA9ol&5Io#_ly(2xtrq5zzhkyoY!BiqIr8`u2N!4tpo@FL z?5#iM^gLpDD)#)-q7M$?QC~dg#An;Ei;GqtT6^>7p$dI7X{FUSU%PS?e0gy(_mE~R z?+=#+>gRaH-yRY6_X%98b58Jh>%WgbW#`ZRXeq@wV@HC0+aay_*R`kV<~=qx{iXgU z>E4gpAiYZhVukOP+?mDuIMhboUOljM<sGGhus!dSs#sc1y+7OcOXHlq8J}d?Ax`xx zu|MML)_&Zn|KReXxOlD~YxW*^@MZhj<FY^Ye116NO8u+C$FAV@PoKFD2lGUWp4YYb zye9Lc4#SokYc^jGeWb~8i~Gir&zsYnqiy$H-@%gVbNkDo&6jPJkJlO;+Y)x`<?Ot< z+agjrZz<JRznQq@{MPB$<)*){DQ^wEeEwR@`mmGB|HRz+|9kS`ON`g!r{4SjWAU!# zNfn#w-)~8urDuJhqwH|;`6cV)%j)jh2tRJ#C-Q-FzePkZ|IVZtbNYN9PcIEQ+`;^c zW3$Vwtsfn>EPk3?F-K!(SWU+9UrJvB{ElwhUX#3ap@f+hleTMW->0I$%H?x-`d^vG zdM<OnWFhp>wVV0jieHy+%wnxsbjT;i|7XLB!krZ|>^CMZ(wA2`)>yb%q%x$Tvvk_? zUgm|f^LDMgx$5<@qyO9&?fi3Er%rMje?n5@v4?XXP0#)k_>+IuF45_7E|o`$I1g)B zR^G~7KJ{6~-#nRsqf;eayP0Ql#G3q8UOYoR@9_aezIBfm?BCd%e?a|I;<4%#8HEpo zw$0tDyY0rJyqpUf*%g1kUN?_jvzu}Lsy)}4m+oeq&+*LFVSPrdY@W7O+|laQb6&hZ zRZw>Hb;<Xz1Iu?8O~crd;IoU#b;F{~?>&!)YiNlb`F_Gxdp=8lx6^6S9gm%^^{u)r z{yL*<_w~5ytv~&2w7$9ssQUdne)8!K26I0R^KD_wcD?IGr&^u5|NdC{qry-7ZG3`a z3U+}8;15n!Y7rE*Kc)UenL*&QOZNirkoJyi66ZSKe_!nNzv}hc>FqU-zABv&KDfB3 zJF%+k;WDY~foH@YcK)z4Ik)wQfumqi#+p0wwIa_Ay2RA4K4!XB)Lq~h(m$j6-Ok{e z`BI796@QDm7f$P9aEo!-d)DmsmAU2}48Acgk#qKUa5+DW;(KzW&g}QOeSfTu{yo;< zZ)m$}yZiiB-#N3pCco<y&3rV+?M(QVH1+?<OFDKKR2^tw)cmGCN9z3lBS$ZMjIF&` zW&A*Rz2Ngxi}WAac(+(Ts`1!US5tmBP@Df?$eHUFF5zD)Ek6h`&X^(AxF(~}PkYtx zBLNYv-48!3+7zIzdv7(nOo4-4P0YraIrF<NxW94#61gY5TkQFoM|%=j+-_wTo-_Eh zC*$lPYr$om-(xO5`r38=WKz`6Z6Avl6l(0Z$QO666xEG0K6hO7xpDW)!rK-tp+4P$ z3Xk^QoGK=+as2n*jEAE0%RfpAJ>T`HSpQu1)brBEj(!wOk)D4|;zQ?zpr3cbH@^EZ zM^oIV<Fi`eUkxsaE#VIo{yeTp`%_c8{OHcpKV<Z_Tim~Kuk~-DaBWncpq&=`l}E<g zB}~<xb-(RDm2fs{&icOZ^>@T{xl)dFsM+3SsfiMK>&-YnS6qIQ-`$FCv+56btn-)8 zzyIlc$=}WXTUVwD8@qG7=-7JA-&!}cUg-DdH#^tr+o>Nli0I92oSEkLy|(!Nu9MpH zD!r=y-F<e|;8j}A^Mi)3bEfdChS}7<HTGYYA9L~F2OI74#tON+E+l2nw0!R8ea6ha z%8*;n_RFi&KflHOI&vY&bn9)+P5c|Qx8FBiYx4aDf0Oy~9>4g1uQOjx{qBDC+02Od zU$!>*Ka^l!H1SlP4A-iPeK&JuJ{)UwkzpuNU2@stOHg>bV%PP)F9m-Nb-QF-eAM4A z@o3eZM-LR3yj{EFohDkn^SYff+4t44=Q;WtE`i1YRz2OcRCaG1OW>);KaPnCOtj0( zDQt+b`_cQ#qVPcIKi11Te0~-tXQ=R9IL*#bm>^UiG39u1Z}Y8Bp%PEOn%?W*5cI{f zut7L+y+y<OJkj|QRx3B2iT`s^f_+(R=Evi1G7cuoT8^7P5I#Kd=v58ZZu=LH7`AK{ zyDjYbWYJMkp<Y#{?&ng!S83ZzhgPj`cP?kL>#dbr`K7{t8viY>$7$QoGUWwUt~8(B z;Gh30${c;s+=5w5M>}8N<NIO4Cbei%-N)Fn8vQ6f`<9Y90{LHC@<L8_JP6o%^yD{- zi!uN5%vZ&@JWxL*@$_2O0re)?#csXU+eMVQM4SsmU-#B(hcD>VQg_|4bjI?JDTOuf zWkR#0qFekQ?eV<$XpY;@>VGrjev65lPCsl=*tCDggpH90(vOOU`xI_0*m>0R;|j09 zeFE02qNeyyIqvy~LsV3^Ni=Zc;yaI=URg|xwLAYAG)d3@^+-+N#HBVS;ni0wrB?rG zsoh!R{nJ$X{qgU+1Zy<5r%i2n#B?Wj``vBp{1<n5R9Y<G@nf6Klt`A0M={QVZe7<o zHd}-jpSS&fC2mvw|9_PkODg2e{@Hv<VV(b@?*AF%^Ett(OSc)y-g&9>K-k_`utMvc z>QT!%98;zq=HK|MLf$OwXJ>%7VA$&f;aBszxbG|Lh(27f!(#v5ZO`XbKYPFOQ{}$2 z&t_rgx&oqGI!@_ovGdAWz57#?oUq&`|4#q>?oTHqKF+iF{bqCa=A*3t3-_(LqjW{= z;697;z%3T}65^}O*<xq4PUuRN%71LULFNqi`(qQs9_`7FNr~T5@&1@W(A?JjT|Z~v zEmM6UJb&uP;^;iVik=PL>Ca=+=9XTw`6FWQzII!5{@$&=^5$%1r(WlXacI@Ix&3!} zvFeT$_Zt`1`JX(L0#jGqVSifMd%l)&z8JX2J%7zAUzXBSZ(l8+@9{gf+_mccV*b;U zXWxrAU(HZ*EG^#Kc#2&}+y2jCVog7bL2Io`PrInj*<F5ndETv0>2v0^=e$ze_Tj+^ z{rn00HviGus=Dqplc#n3y}Fm1Z}r6P`FuF^nooOOW{l=K+jaX)V&{ea%FHoNZ(Lr= zrZ<DVy)8wwFYdgSylcUWdlQu}?bYwkn6ua4=G|(>`3E}Vc??s0Lq2!L=RJ*&l6`oz zF`#F)q~p{+t^G$DV=Ns+*gMv?I6i7vVxtrC$Ec70$GohM?OrkqMAz<Ba8Ev<{>|R* z$!Ye-N3CYvJ?7{rvq${xIqUal_@8tgKbpHZT;|QuJ+Zlh2RbhVU3_$4yNpVg{T?NO z;4Ky>@=fmu9GuQ^Ktz}C!KwN^BA0g;N!N=^3BI{;@5b7rjv3+CE=nkMryetZARM~x zRPxSa^41fd<%Owj)V_TqXWl!Onpta?ufF{2R<-T6-PVlrcdS}&<Cj-$@BMb?5qY!1 z3*mCR&He?vSR)i({rdH(62|%KG-hJn^149BK#|GLu%)(!J-)^DQA%!%Nfe*!k@DM5 zS6LaR__QcQW%gdxR`L-PHB<_@d_&6r%kj@^*=m$_d^%%%{z{l^;)E4X1nys+x?g<F z*$e09u4AjY6}ji^6ONTr+~qnNyjg-lZRR88S3cHeBxlF?v&biHY04;^SjHlMO4rFk zP%JCw<KDWTO3x(TF0gRCXOZGRhrJ_9yjNGRsP{nNMX`v4#+nlw9u>N8?`51X;ih#@ z`ziO7?x+5CKcDO>*7|6hAEP1j{%RUy>x!0mJN4i-%MD!wFF!U-E@{o!EBT?*u4~^b zzt=hsgcm!=R6J<h8tQ&vjSWl8+s-$BED?|X#9h3_y-xJJ`R={XW@UfU3NZZS{_*F< zoP8W;UF}`Iu3iywC9eB(?$c9FAAgFACY{o?TYYNb9uBRk=gX%)@BQ5?TJ`8p#LguD zkQJQ{?v*pvMd&^2c1Xy{4^i6n``zx(wLKGpc1{Yh(VJi2oqBxv(Vx4HB_7x%yh116 zIh@JvW8}?<t!jrd1ZQ-fe^KzS{@Xj2sJ>PAzW-qR`iMo*h|A^B5{DT(l{mB*MLgwV z*isFe4zW&j40(68`&4n2-F)E>HC=aFxu*mg3b;778MGJ*rUbAFvP?0MbYNi-WsR@r zE}MVX`fBa2Uvuo1`+qNAKlMvUXzbUOuijbht^a$U=WfSeR`D2tce`G%`=Z%(LHT&V z)oJy52K9D2(Yt({a$fSksq7JyepFjCZ|UEYKOeu`^ysUNgvA|0(dVnHG7l_Y6rs;r zV;3#G>6P;A{K)%F(ei(tE51kG=Rbe-p2MwN`5Oh7KlHwBvwA=C{f^h`dE@8Rryf?C zR{u5o(0cwXAG3E6)$3-<Rjx?frDgu__uc<u-zqL|`K41_S6prJW0Cmr#I^TtKAh?s zR#m&B;QXx{)m!<KZuMs!u9pon`g8r6_Ie?|SsSY(N?)dW&F@S+-M23*PxbO8*4|HZ z_sV?mG@p8|%*l>ty34BR=5M~Z*v&PIQ=fVIq~gIDdj#KjPQDd#&2#mlCzq$+T_z%( zs+|1$>iRm-KKp+^DvFOxpCaOW{7R1bgJ4sM!=8n|xLN%qK3%HaDX#9?E?-yiaze`7 zJPSv?e-k=;CzbuwXLyu+R*y4$Nqxu`skT)&jUN5|8Q`t5&(k+NljX{%LJxPT4-bE? zym455%f`Y-ZV$BnGV9n~JNYz%y{77AM1R~|mYPqG`{NbQ%r@88+qBmEUx813?_v4s zm(xXZb(b7+VXM(fuw2Q+rLmv^m2glr5Mz|rdiFVa#|@6pAC&KI31pn-rLi$|Wt8Ca z#OO=!7e<%%e72Q+J#pJA#`!gKA8&fGC9eMO*TZenc@aF!&gyk<Dta$3nLd}LX2lMd zrF{F`mo4J?tMo^?#fd|CEt65qf%b>fKAe+QW?XV`&X!lckpeAVlA)^`0=~Atdbv?h zK&fG}d7aGcb=-_vP3CV|R(@*mUmd`8{$Sa1-8M!Ep{{*xTb3@I`=##N(QR8Fc3g6s zFFf^r=3Sm2AFLkTc)eum+v_DA>n{6QuU&Y)Q@(k*{T09Ylaq~kPhDW2&R;vlZ+_hK zAIo0x?#oO*q3M?MKqB#H<$T4fSrT!2>!S`EX(&0&57tpT@OsKb!ABlKA1goA9=3VO zzx8p+$4M8JFYRmI^tii0>bPIcJB$3flZMVyrJbBNPSw@7yY_0*_l69Y{lf2gWu||z zUiSL5(J`e5&aBc}H=p#@@wQcXb31>iO1~oLw!}mF>K2Zmu*}r;nN0J}_C+_jhE#Y* zOW7<@U_L5!>40S7wlD0DU+#%X-=8Ee-MHdUt(u+Lq|-kvCx0vNPpN!+PWrs^t+{nu zmPIexCM?mp>hU#i#`$lvLetG+5|{qkHmCafdTsuhTU~PB6_?e=SKOYm^6S%>{}-4O zpS=lwy?y7VE2rb+<ShP7=KcOQTH5dRqU!f~JJz4-wY>RxUeW8b5AF5$Z#bpowY>J~ z>3BiC`umKDZ%>{U?Khrs-TK^*yoy!d+U9MGy%wo2U{rN<lJ~N5{j$H$?5_wt-|Wv_ zU;R(M+tw_u+-FLI|GUGp&E&VuDfr-c^v0Uh-Y%OZDxdisoSdc{=<W^b4?QeBM`X!C z387hYzJzA}k!qOC^{-H9_P=CVOV9qbcN@;nEx#8zr{~Dew%sb3bNW6<h<$kIGS|>0 zd9{>M*XOOf&S*a~xDy%Gx9RGhkA{7$c3+YfyC^&^EW2cJ@X-?eZI}J+ZDVS`Uj3LB zJzeFseY|wOdG2Dt4<`?&>dfy~@%zEyR>)IeQ})a7YH!1N7t3pp_q|ei;5=!*LQ>A3 ziQY!Ly_7G`otI;@PCM0X_f?)BCEnMbYN2P72ByE-0@lxVS-OPBEp2^&_6K9Ztrs2h zjMM$9!ZXBdGG}P)oXBpu!t7i?LTYavn^orJFw+NzjeacDwwK7e`s3o;9!cXa`CHRM z#a9Y_NPMBZPknjwEWY1mw+a~L-2xu8->5vjdhQa&`Fph3uD<$w!C~K?KW0<Eoo-V) zy*#^Y_sy#3m!BVZ-JEz}#`@3?-K&%`RmJOe`2A4j?>O@OIs47b3te?HBU&~1C-yuO zh<&^3$X6fzcJ3bnyH?)%FE;&rc7x*0TbVV}&rK-Le9V3$M6FAaO-?za?8`-W^A`1r ze8r9_;$jQtYyDNU`|E#Prr76~$kDI<^}jB=wr>2vV4v}P@50E_tFN0_@3Yv(@h&0e z+O``>-ANtRbvbj`lxFL$&yl=y{PTx>?q3&|xo?!{Zs31CZNJ)!-49Er{^OYUGGov6 zkC7AiwS73f`uQG<($D`-$6Zc6ta(f|z*AFpV(V|qXDg+|ZtuSLbzPg{V+kd_`Rab& ztm>;KEv>DQ+5B>aa^bCsamTK=G0rzrt=4>S`I#2K^-b&R5!Q2do}TyhrNq7Lv%+Ha zi>qHKyPLKo{(HQM`|!%`cVp(Se>FAx+uwkS%de-cKmYBmw0MhtjB&`Jwf`KBYRBI! zJO1Il^!^B`53Nzpf4(a|$8+88!|%4Gdp3n-y>H`}lM1Ul+x_(RzaQ84xBTDnCjH^7 z*8TeLtaq4|+}J0b6Ey$%=J0qiza1~1f4%*<HgyqOO_6z%$dtMzLLT=l9G@K4%~hHA z{eJ!bFY5&3)n&axXMf=}kAK0Xy(WOi(oNEKRmp~XAI}=kH`{+V@4)kG(fPbnT5`63 z^k%U0NYdu!czU>DqMNRDXXoh`PmI>jxO~#+Q{|~`s`HoK%sm$2cRDxds)m01jhuTG z_P?jaryMaX+F5VHWT%sUZB3->&9HUTP5%Wfc2WNQ>e=jky;Jk2`G%L>&tR!hd$m!{ z){67ThWzRAbuTt+=w+)+|C4AsYpKcSQo{$6yDOp@Sw#XIP>BSK#ys|#A9{1%YglKt zD12J=YV9=ViU-H-{}t8>)rHInV_jt*u-oaX#H!;<)@B`OuYBoX|M`q@cY%=X$+c2{ zIefe&)N<dl)kLJX{o46_UiGD$((69QYW$VXJYHwcWXH47O+d-4?t{;bMP7?naV+fL zBG~4&zu8~@^!i}QtXEf7{=D0L_Z+hwkH5=&$4G5yw~nsgAwSM<pZW9hE3==UUUYlb zZ|DoJ(e=5~;Qvo*@%@yQOQ)SO-}}X^an=?i-<K|1tvSmCvOM2L@n(i>i<hu$6mYw> z@88$;;s<wB$cG;Jq!ni~E&SOc2S+)(p3@JE<l`nJ9=fCuXr;T}yG?g-eV5(*{r|qM zcfGG<R<qyVRcjUN(!UEr@3B|rKlph$z~|SWnB^Xf=Vx!*$n`^@;PLUd8x3RR?N8)i z`*fYNRm$$)$9{b?%XKXwr~8g>GDzRN@N0yDa>EkQXBGBJGRLa3bN2AxFT0)lG410n zzx({J`LC=C*rOzWtiSI5+HJSA=H`T~J8qZ7Y?oW~E%`wE!<BiK4?eIpp7pb{)2aD& zvhBCno9_=dTzj(cVd}nJ4_1BXoge;Nz+b%L`ayaA$?-o|{feEx<IkcGf1~qP-GBFl zE426At<v|u*4$s&T4Wc_{rt(>3uo^M?fbCpZTD_Y#@zB}o)5nr7c=#l#;5r0NBlhA z+4b+OAI{uwf8F40#oZUv8~nd0U-T$Yx1Fh6aQH)XrqSGu+>dXx7v8(LL&f*+lgYiu ztxw06-AwI%e!J@R+S^+HBev;1mi+M0MZ-4pS#ziG!;3j@7N<RZYgH@f_w8~?&F<5R zw&&YZt7HOi<zHNM?0T8?>BlSuGgI5z-bt@*;6Lwc(H*}jKx|X!W77lcBRvFzuXcRN zUu`z2;k=9ewZ-078~D{P${1H3ve#RG*?X1SR;$RjB|8i4nC$`&d1IzxhsHA2U9uk% zf4<xh`F5T7<&Te}tmfU$Ri9r|lpwL>#t%mMx&Kqw>YTXfc|bkZZ0>8&lD7x#ySLvd z@-{39`(<(cwAG5+yg!~WvhMR%i*blQDwVw^u%_IX-Q0e`GwUZeb_sq^Tri=p>p<~G z=C?L`M6NG<GwJ;<&d8;S%uG?i%X~Cv?|J<DQv<)fUj8-pN#T_bJuTZTrqz9(eP87M z!{yqSbR-<_2U)FR{`?^-L4++zE+)e)g@0L3fPnO0narsbZ3k{uKA-#e@X=Ir?H>=O zg-ySixqNQd!Yxu^2`%O~_Sq~bc(d`im~;44-B}N$@AocWxNXyQNzq^lMwM+AU!wNN z>Yum!{l>vT!|cpTqr(&GD;MAI`gd|m#DV2@Uspado#B7`T<$bydAY;yj$YI05Rv!S zU%AP(K*LO{_+q=)jFyYRFYYTjxSr6>J=^!VWLDVq&suJ0ok~EvU=3RmVy+b&?tgli z<x=~!eR2_ej5e$GUd%s}{cgqLzNnlP4%gZ*`Xt>hy&l`Gc;M07FGl<u17kDZ-P!q4 z<!JGe?+?$sUbm)j&PDZ^+ltRws{7<B&zW)V^0`uGySEyum%k(*Xg~Y=ZF@h@<Y`h; zuZynv*NN!u%-4uKZlZqnhOWVy(43I>3%mT)-^ORnUBxf+FFvs|<(Z_tmFb-iADB<t zUR}3uotW6JSAW@Cr^c;1_4##FLVCM=oxV-@CCz`I`D-lJ?0nOmxYWD8-2cY&J)6a< zH)Wn)FY)ZG)rPwN52ybzne+8^^y}@%KYQ(Gt=aJ8iHX12C&zQ0AvrCfKMr*FPD!hB zl6b$q>g4PSxetno=L-^4Vxmp$T0C2ek`CJYF|7C?e7l0J#=~y;()&M}mH!EfU(a7M zUH<ot<4h-S-?$;Kto!lTkH`Jyb3%@$@|SnocE0{`@Yt6r6H{s)t$GVO#^Ufk`LhlD zuMf$m^cecwXjeQM@ja|J_kp+uzs-d|wwo?D_}k08zPK*y0DC!;U1sLh^m&!b9`d*B zc%wDF=c`rqw>MgwLuOrT@K@DS4?`=}LEIIbN8Y!&|N5J2d}hxs*11d1?u&nJUAm9m z@Ak^&^Q4yf&YpJf>X)jx%0^DfRq21fUXRa_+ZX<{`tg=tlLwRax?h7fAO86B++Lm| z@krRUg>%_!X7M_2K5zG1B=p|nO%if%7Bus9nZKTH{bqx+v*?dJmYN^?ro30a?Dllx z%gf7`my2F6jM$Li__ywxx~O-?Pcy??4mE3LB#8Vw&1_fm-aFl<v3-x=8-)jg()V?R ze?7ELUbV>JNWiTZJJfY1Z1>jRyXA$q*Zb!`mPFb0GI&c}$rPJ%(m8vLyVd;&_L@%z z1pi88Zk<_DZM`=xY~u2p8;=%mh_?{CJc0MH#nP)yFPuGp3to@PUaKS$w5>1Xk&?=E zwl<Eb0(vK<ou3Ns>F$;16~5=uUcAWO_-+59TcQj1Y?{_S+v1-^Sa?j~(M>0%-zo2y zy#JrXtDMbe&9Z0oDLk52&DZ<jaC5TghljS`T7>;ptmhZqU9on<+{(wt-+oD7c5u$0 zy%QBgew*I?p1R)iows}V43o?t!CMcH-^)y%FU<Dq^89}x`*-JLST=QhQC!~pDks0{ z^6X2yyIza9?0D3r{p$PW@5lc9EWWNTad7Gl{#oo#+%}xpme%`RFxS_nOISDX_Q(G+ zCqMl%eNg<m(fvyM>O;>Tbf4n}b%L3%PK?fxwcC2t`qg*a4_|^4H&5O#YsGjs^Q?JW zaCx17ZKO`}S>b(x|Ng{07cKIex3T(0<=oQlRH0Xu;ktJwhd+IIaQam{5l->Fd%ivM z{{3R9c#CoV|I|X;|L?`OChiHFza^*eyW#bC;eAH`SJg}^+<CX3QQq&1q@O0Q)tu66 zk*=J(JA58l+IFULEUw8pXYtlU@8^Z3N22~YdIiljG?uUX@o>=<DRp6%s7w6yLJu=N z_kHB~VGyUG{%>R9CMU~X7jx#k*z;J{`91efJL$8}v(h8~2u@{NV_|3Ex60(0+qrfl z>9-C3yX;g}T3Rj>68it+f`2@FVN{>#KL<~Li$}>ZWkMfbPEWSADkz%qf8)8=A9sq) z*ZpaD_+->-h0vOHANQC%FrN3zZaZqX7}QHPFl0?S9%IgAmn!N%EA*^G|9gv<F~0kw z^;h4x;4|y}uAg=$Z$Bk3VfZC+iSLQKT&2pJ-S7AD${yF*>~*01`JzKrIU9@o;zZ}1 z%b6y9TWxlZke}7dB^8@@oME;siI^U``MLG?l9J{<n`gHM{`@<^nJ+T})F!fVj1z3F z6WC^2W@OB}P}eDL#_?m@j&5*`(_MU~{pX4)PV7~8w%4@J%}<Z1tDDJQQ!z(=+Gpnn zCG~<e4|XKXO+LN)?-OU9wL729a=rSkBts$2JH%w?-){^5g&cATY3%HMZuk98@#C|P zj_+^LQS+Y{v%g<J^!Vq+{dQ6N^Y<{@v2@)if0_No`u<P92c^@@n>1$}+xGX{ZFZ)- z!=_HsA#**Y59cloExmNSVjk#7ftRt0>lZo8Uj=O#{$=tey>7+5H6NYpo76XM{LjHH z6wuTA;AGwJx8YxSkNrNSb=^Say1L!RP7BXM&-+3Tr*8T9@B2dj+7$=i?0sLGAE>J~ zciqjefBsHOmX`iC?b5XU>K|`Ju+~^CySY8HUhHtJY~c~Xms(=iE9XDC;U(R;s^rTA zZv8z5GPPeWzEp92ptpX;;ldNy=KJam4nJ&{kLyvId~OcAedXT5Hv$~q((4Q3*!Sok zb<5~JKkJKaYn{iv<(JN1|8t|~bN{i_n)jv;E-y=4&r%Z?{rbq9ZGQqfH`MR>P_yIX z#DysZvu6kU95aaInLc&f-RE;8*TwzV{bk*UZ<@h}Eq9wQ6DzlU-c#_Fd4K+lb#tXN zCE8!E-1hmKahqSwtw=YkvvIF;CHDPYxA0O){GMl@KCG1%Uo!d1djH6@pdFe255L-e zzxo~jp*_FfufFfM{?8NJ_x#aof3-bYxB2}ZU74c7->OQNpZ0wF8prXYV3N#zhZ}as zs(k(1tZt_HeYh0BE59mJl;@P`vit{g5|*62<2P?}j+~u`<;A{U^ORLJcPgLH{kU$~ z^q#M$+g)=&4YI>?gm2y8P?_2rDJlQ{iIDo9q}k^A_f{<5@p#kiolAP+s~(;BIC<Wj zt($vR1}_&=Z+*GtF3*o8=MR^9%@#VR)Hrwkgo&@zI%BU{asK#n8q%S7a-1i=B;f6; z>9ZN<$KA7DHybl2U6{>um*+=-;9M@7je(x+exJg6<v%aGf7sAOXzq^8<(aF0e(tMW z_Vjn<KJ};7p;iYaFR8BO5}Y2c`XJcM<UG6mkAsVh&BNHzrP{c>=C593JbQwJ6x+f} z)>Cu0-6=Yq<S}>a^|;*;M{9On4Uf0|;Oy$8Q4;fcuDN2{md|q}cZ+_|<kkyVIBx>e zU$c_D`St&PCjR^LbK;I4g4&AfZ#;MY(9RHWtKa2E+bZ!E%@^&7^EdK-S$)K<Rn+R# z?aWk0`De;g4t)rdN&XS6+s@#0>(eR2kj&@1A8$GOPyUeI8s+sXx18d;3_3m|_eH$b z-!GRB7ao_LuD48LQs3j|$5XoWeJ1l=J#yr+?1x0z8)9uPUA+N9wIwz7XJ1co5Ne!g zGRJYNP+#kg`;YgW)mu4hQk(jWJWx;Wsq(6Q7dRvfbwHbdHQg?0ysLR>e@&k^RX}0q zw_Dk-Cx1{ba0|+~_Gtsp<&XcqUhJ>iQn9yi&xe20_T|l7wx)~Q-sIVgWrzH7at{Cd z{XRbA)4>+4@Gl>i9M0al%0zVf<hW_Pp>Fj;_GQoZHtln`*8kX;H)CVZMy?+pVk)mH zzf;yZT&?<E`pZY5Rf+P;8fCtTKV0&q{X>rMwwPnbOrmo(y6%bGzl2M;cIw8BGQV<H z&O2JFm1FoP=GUCtdAr{x+5hSP9K5sZd;6BcJD=>%X3zL;+oiok`KZqJJ4HudEAY-) z_PPH{)tmX|4=yiDS<g}<(f8_Hl!m(OmDI<R_le5=`g}Pz@8pFPmI#hbOLDt|{W!k= zecAbC-J(xtbGLfgpWgrLsCkIol25#%1*=Tjn07y2(9hQPvg_^tGy9EXs{dY)&fBwp z?rBYLf3EO-{|{{VvR^0E$L{~Z-^>RWi_a3tzLmE1>fxQ$XIAO|lHHncQFd!XV5E1u z>h6CF*$<t$>8!pY^>Y8!+PwlF6z87m49yAIbK>-rqsMRURdG4U8J06e?{SNr*`$jv zXUuQu`f=ct{enkd=Ei+$XH?JF`0-)U&D7~fXY1;JKESPBnVR`{`@XMh8-;h=kXSuk zAaHjt{|;t;n+<lB4;TMYnP^_IYUUF8rX}r{CwKp1vf8uhqMLN<VLoZDGl5S#p1i8q zYO~=`XUwnmRO{ESzozj&+tQnQK;2GX{g8}urqVq7B8z92h5IdP)fwm4SzcT0sLm*V z?|6lMckhzws6&SL?t4#XtEtM&-=T*&?0evdz>Ba0?eDcD)zsJCIaGd(U8candj@DL zs!a8pjl7pP^o4xNU;g*o_Wit|voc<mFMau^#pvDc_xswqEJB&=bh=ORgq-n|{ww-= z?e;iB7qQT~OK!{UM3pYpvB^EJer8$xY{}$n>p$-;dGr6k!z04}YwX3=2h`5<djF+Y zV1t6hgfRX0VF%iur*CPH&fB55?$n0qE%l$yrQdlu<-GJ?r!Ub<ZXYuFBQfp$_sfYB zK2=`6U3nnA*Yui(lSZcLl9FE)bGd%AC|%koBKP}=y8X?v`vRAR%}#88->E)NqsjO1 z*VCY;*~3}c>k<uJlVdM^xDdgA@u=5^b0PVsnp0}{&+j|geluC_S4N-JD~)yAu39X6 z@!3OrzJc=8*&M4sSndA*@3(Z@qHb<Al^K0W2R}LoD7T&9xFG1)o!DU!U-xt7`I==M zwNJip=0Ei-X~l`d**n(=$|<shn#JGza_4w;>aFwFa&~>Gi;Hh*{d8<X$Q$!{^Ct#Z z?Dx1G!B$f-KdEEGpTCB&^Z!^W#|iw><n9pMwCMf}g{Nm`8h6W|e7-(%ntKNK<(k`{ zpLcC9RQeKb_D1aT2ANe%Q+xkhcq5TGMdH_+T~{9&&fR~!?Dfw_MUN|8_Sl9^uQheK zz9*9X+NvLGBDViN@>IO_{r#xDuMSMV|M6VG1vTfFM_c`ECCz@{`}p<gvhaOYwZ{t% zxXr%)zUtS`{jFd3|33TS`mOFQcfbAPHRC*<Yri=0)Al)^?mxU||99iVePww%k=4bj zYd&9_zUs{eJC?E=ZE3lWMZ@`~zcVgL`uT42zWZB#U6kDYM)Ka)W8IxEPuJJy-*|o8 zK4;qv!@c{(ne0}t+x04Gsq)HYhdtfYd!OjPyuD@OI{)L@PYr`!);Jt>%QEuab?mqG z-X}|DOn&j>*@+^LW5zn+*1bhNnMox!%yw(^A1>N5<$>z-n4li+Tj#bJ@TwnpDW6o+ zur&XH_S!8$&YmupHrEtPRFLaGc2atOMMv*sr)%juCO@~Zl|S}Ja01`wqshv=KR&dF zRV_N$>1(ZCF{j~^d!CiP`s{w&ZxJz1f2Ce+@ZT@{+Mz_~L!wTq(BqATpQhJu6p(*C z)tlLF?<Vyae$0V|ghw1l-j{yv`!X@_pnm<&({1l3MBclZK0mf*`<<+tsfRxcsClgZ zv3=J3<9hpkJbLu|Wt!~HkVgxe*cUc$@$$IZz<*!=^2Dm^vE{tmy<G1ziP~S1&pS4g z!{x%mx$J5cHoQMR{51@<f4AdtpGEdz)xY9Ae_b?K|KHdp`C(xccdbL_)OkPl{8Y7X zS+-yOZ`|xmi*G*`8o#!^!@vFI3w52%?{>X@)}3Ev&Hp1HwfX&7PJa_?H;%-QwFaWD zpN`cn@8Vt9`%voes=_zR{u&m_IdQJ7Q}Ab1uj@JeQC_+XR9(gXD70Glw#8({xhBS1 z{!ppEVLZ>np6*?_W|Q#oG^;~we_fBu6rb6!`q0J`h7L=oJ(T+E4;mIYovX7tCU{%S zuihVlbx}SGb9Qa|UGYC9;&9qSQzj!3289`uwr%A3@nKKM)`zu^Pie3B319rhYQ5~e z>!<End#7g<s{gg=$(Xe7{d8YHdG({hvs$Yk+2pYQUZoQvy8PiJJFULdRjj3(kJ{xh z+vRF~_hOX~`(D4dw*A9H-*zwmyFY&{et0*1uF$-nzl(G8d^z&#{;qoX=leTxj#Edz z-Hp!Un7^+mZ_mwwod>zJTTkEr{+@Sg^S|$<pAY@t`*2s=%UO3TuAYt$oc+IH!?!0- zUrmk)mVT7IJ?iLOzgrJy=S$jcKU?-S(%$I%pAUzv>q7U(mZjc|VqLh~z9!}GLjF1R zWj}7(K2=XG{(jFsXVazE-I@OXuJ%6Mzi!74yZaS!FIyY<Z=dfxyXmxE^m2vF(-EC- zS;Su%?=4Ba9$TJUaX49W$>PrmY@Ib;XOAB9WL2D2Hh;44X%o|v>~&EOTg~H{>|S=N zGfv#3QWVo((aD;BXJd}S_x|z&>io3@O-DGxZpsT3T3_<^>do7FHSFu|A0PaW_aE&% z;92#_d+XvYImxxp)WS82Pp;QUvVJ-xSd6>u=<&AQDu0f0GW<5&R<mMzMg7gxI7v-E z<4X<v#|1J!-_VGh`R}3Vd;Q%iN8F_Bwxu;1pPC)C$Y9&UZM*OHm2R6odDb2wv85)n z|9(7X)50ye-P|^9)f@R|kNb`PJy`NF?#PbyZ}+5&7?*sQ&p4m&!W>uB;bqW3ghWAO z9&62~#F}M(b8mg?_jy^fOvpc{=D1n;yE}`FeD~z<@U|*?d1U#n8kv2qUHTzRi;Or< zpJ``YCG{auR^n3U0=E0L(?g3ET;gElU3K_!=AAVcANm}4`fBxhy;Y}=l%8AN^g%gl zm6w23hy9ADmdku+i~U$=xKN;O&g{DLcg{BW-;vW=@cD5L6I-Q6`@%^E&)ui4m3n=~ zPT62;yVZ}r`3_>o-6A8FT`fAzWLHz}eP8r&!pA-S_J1FFXlSq5;FPe0wT<I(`?j^W z_S`D4uSxf*KmYl>ef<0>3f#5IJ=+c#UR!1OM7&n~W&aaPvzL<>oNwD_z+|-XtJi_{ zuM3s<Zu>v|T_Uk-$GSf+=E`4uH1pfqqsMRWjM<VAxL}pI=(<PQO7r_ug_k_q@w{|` z`LCc$6Zo8WhjO0(@XPMskH_BICnitax5lJps(pRc?C;8F*6q&w_^)_da@O10N&B{f z_Gippd#QBQGC{wcpfQBws(+_Q=kF;jjQMOgMNB2(VY2LpPHC4=xkJ+`=j-=>wo0-W zJhIs9W`=2BXk0YQ(@WQNPp7l++9_AmCs?d4lRD^qZ@Wa(x;GN3^Dos;|5m<j^Buos zh7W=R*G^}$yL^9x+mDNR&QrB+OzD2N*IFPV)66Dx*Urh=hihUtJg-ZftCI71`}6;T z+kX7x)NTJ|Rd+o0(7e}q0r6MtvrZn1J!hwT=fj8QufJ!-T5b#5nSMWtb^eWik-I;g zlb<6~xAVosityXhL-cno5j)W7Ts!yw?fWf%^(~gK{aGK){lg$c;;r`Q^<2ke>_fik z=`}q4_iI6aYigFd&zAGCwUIy0`P%pM_rFdBH8kxy_}^^#nCP0Y?Ct^H&(G#wy%TQs zMC7~hyoV|pwhtdM&lX<uP)gmKrQp&A7n@F>=Sw%++cN5}R+-{=zSM4l=i8uDhPiV- z^j;T;UBkZg>7@^h-woFpI_lW<M44`DEnv}nbmP=2lZiR0x04?)ndjhhQ{J+0R)y!U zc_O(vb2KJ3^{R?I{u`?Id4oi%q~*z--|tmN>$UM0Zc~<3(>rHyse%9T@1SL7X5SqD zG+W&ZFFScsqF&+nF;C{#5#N5@s{8%6JHGx%=IzS9I;(4s%Zy!Qo?0hsTc<rv{%CaH zKE&fu>18bmk>__)r^j{`P55Y5KjqWDOJ}F;Pv5qA{qCK1df4W87JO%Y^nGIGf<@o2 z@7Zhmq&jn7|8n^SZ>;Zqn`ZPUq{jU8_jVScAMW+cq7TF4DxWU8T=7HI#P4AF!y0z~ z_TtFLKVO_spW)Q7>}E;o=d#as*QE2^>=#6FI8NX4e(Buj%P!YGJk;u1Dq;1la7q5M z%egsXA39$jGCWj}E2GR(v&t;a`p=6w`}nU-6%@E6-?weE%{SFr(J$_6HlMT7)jqgo z^PY|K+Pu`(*gNv_{0J}Izdb6wsYE86W#JF^|9&-cP1S3^N!vI1Y4ZvmNw~MFH+j~> z1rhvC(k-1<k$mS*e@K!kj`<Xo7GeLdH*G@e<4MVruKVsjd^6Nka_vh_HleyhEw!Gk zx^eD{D|w#2K4s`K-}v{G+5`WV<WD+~qOQBI|4!NM+@t(^Z_d}5{C(=iEVddgukS$z z+C$GX8{a8+&DB_W{@NjpkT03n{vE&dx^4Z{w%bt;uddIW@dUn9rS@lU==3<XGxy&= za4?;BW8T`ml`=&iD}A*e3QS9k{T`LS!i)X$xqY9v6}&iV-`Sn{d(zX`DDLlTzF!hZ z?VeYww&(4a_igt7p5ELxt?=5q>h`x`@tbFSEBChxY?;L&y)^f_`8xT1yDzW%x;yQh zohfMU|Lg6?l}p2!>{M1tdK>!(UOwoXdFDsj%`;t-?M_VzR8;f*`svw^r~36fEjDV; zHs?3`(B|VbH6?$uy`P1qisihzAH_m`Rg-^SKDJ}-ON;$eb|^0iHq#OM9LU(Rsb)!1 z<-Z@tP9@J_=t)1&UaHXIDxsabMm#g}SiNp`&5poZy1YS!m!`G(Ui#^wxB2hJ!lLu~ zl9vpVA18mD^wM+Na`nSCaVHrVBuqSA977JU99i+ec#_+WW(U70&rSnj&cA6-oh>J( zRX_G*Ii3*ZR<wjs-jB(%dwK8Ihv8CZrOuzfaZWmSOJJ?gsl;cZS&y1k_Fn#&c9Y%k zr~LAh`oG^sr2pplXxNgF^XF0fqyuZE)Qei?Ug}EObMI&C8Qbr7f~~ZFw-?4;ejp}n zw5@FO<25CYvUf5MEN3=J4+K?3sI#AJF$?Z9+cEE}zWZm^fdWSPiNC@X<UjRaI8pGI z>F?@?)4uL@c++0ivCiy!$$Hirj$L!U&->n9A6vP?c(QNhKCy@&A7jf~;;*cFb?5Vu z$BhhUqPBY3zSiCDbR%i%)kC+J`^4sS>;JuL`|z**&p^ZXN&IQ$XJTIaK72BBw>i(- zqbi@}_t?*={H=fZ?$+luo38Ejk;v%2zFy33!^35XORt+uyZY;Ea`&M#H<!0ApZ`00 z+n0y=bq{lIw>z6xd)FSSJ!h@G;on=cHnrb3=Q%gF=l}UL>%&#n@T=+ZPq%uv&)<`N zf5WdO;_dTyWj%a<NUr$Y8hg8Y;Z>>+nh$HOUK7;A>$P;op1`b06Zlgv{BZv&c*tSN z^Op_kJ0`GIKl(G};O-BHxF4TAs^_^d=vhxgz%38E`!%1>x&}>6(LX3SulvE_wAs1W z1a2t&aah{FX_a2cx`kd|yYhVeJ31OzW&i8U&Q;&B@ow4etCQzV5tw6|9ag)wO7X$r zG66l$_KqZ0Pp9BXlU$5nCOYx4EHJxY^ZDqpt*RU@0i7jfk;(BJ{6D4Gu8jCsSakHG zL(|mE?KjhAE6tp(*ZtR>^Js*UaqXH{rEN)zd(!WDunFHtn7#7cp?}JXJ}37?tbFlq z_j^4pj@%6g1D0-@$X0WMy=CU52NDO3yqJq(9v=Q>Agib>wEun0b8G1~JLM%uEH<2F zQU{&jX=oC%>+BozY3B`3e+yh^COdB}sPQv*LU7!*NkVJhRD0c=bWJUCu6a|N{2E@* z>1IuFXHLJK!D6%D>w#@g{NeyE6aBj<eypDIJ#}B68MEEpE2TFvyW|e_ToNB%p8xwQ zjF(g5gXAWOE!lq_*!*p@a&B9?*qvuq`|aEJnb>Ny-rbMm{;}bg_Vp%X@$0K@3V6NT z68QCb!~C<H_3KJ&w%z8tSJM4q>Gt;*-aX;HZ2RfV{KLD?|EjC_@N&7-yZq<J%vP_J zd-ldqTj%4YBg^kp75_MYdUm_xEZxuS|F?zTNPHFbwSE1L|N9DZbx&+AIy;feeaA7g z$L`Cwn6gC9*>`*TUXc%p(VWTwvg-WhA*Bi*K3tyv&m^YcAnW0CUY0g({fnynuDS(0 zioX9VOwcCNEZ^=-@aC77ey`v2=~U96QzaGi;y8Y6=wq4NXYr_`ZQ=1trTL|E3oc*Z z|L^NE(W#I2ury2jHepMB^!IHHsEKp5*krZFm2*ayGFPl~x~g<caor^jalvPm_kZ8J z9$=Zigi-!lBY#X&GE3JfDMtBan$@x&Zz)(fDXa)h{>d}*+M21np4an_TV0(YVZJjm z+pI<G;VRp2Hx_e*na<vR^>0g?M6kL1>8)37=GvtlSRQQoJQzzc5PqQH{DXJD9d8yf z+cj@mE`Hzd7x!gB)gSBr>E=sniER8Kb8qp1BccWmf(!3|O*+u-TamkV)`{cy_LTnk zuq>a?d+(d4x%pR%w|>;!U;6mZhpnvP?c!OoCqi%gW$!bT*?zcZ!=2S}cfYL(x%KnK zozLB?oN~{9xzsXwT2<<fqHB$=sS59QefO{DxqRM6)4(O->ah<g>Su50=4^c>J?mW8 z`M}S5e9P~ad0);v_P8;Q`$vMq)9I4NX(EkXB^is$YC#98D9JdhPA-#vD95B4qL{fo zaA)hLnvzf3vvQa@<J-0`db~e$an$jh&*w>lj&ex4b0&`G$A*7Ak*+UPWxZBkE&C`D z>i=O;m8W%Y?ZM(L*2fGFihle1=H}+^jas)W`^uIl?l*j<b+zV}rJALg#I~NS;C9)v z8!OmrBJ#B)1m#X?pV@LtP3v7jjc&w-1y@s!CWmAyJqZpA?B+bhWcRmXO|b6Ev-x$! zE5kQ8_|K{M_2p&bg<#{u7O{I$BKB2&e%AW=ob~#b0*!^YF77gUU>r3`&mBu2LWYsq zu6gr%_EK-&A3U$-@jpBw?60Hs!@bd&-*(Fn$I?A-mooiTNjR|l(Y-fU8~pF|fQAPD z7tem#_~5K|_Th~t+4X;3$9F02`FCIcS$T-7dhEMTTNj*pID5b9n}5eLAAa>+#UUsg z6(GH@d%>AE*5dL~s<CmaU#q{_;azclmp<R@!v-nF*;7mNYMv|%+q|Xb&C{&{$J6ia z%AH}F``F;;)b*?Uy>GniyCsk$zyD48!)f+5G5hz2ultg8K>e1$8-*8#16+81URia~ z@Z1E)khcpj`7ZB0aBAD_yl9r%Q>%^(C?#@eX)znBgibZvYSwZ&uKKO0#Cl`?UjdvQ z53BWdzu8n(IvX?t<JzHQ=e|DVL+9(ms){mB`mfjR&U<OMi0R~^R_^Y<L1t3|cdgm< z`VDiNzLM<UHuF7+hUU)>_C36P<GW3`xwLcm**TWR22aGCS3m7tc#tDw!n<cPmXvBg z5S~9<abDTI%I8UQPicKKkPYp9c5d$N4RMA4!lqAF^W_2^2ROqlH%d^$Yq>7>k0Zx_ zzm0fqo3VUunbxYXqmyN~mF{TZzii-h3$%ZdW#g>Ys}k>C?|QwCTRgTT(6B`Ob8o2e z1LLqucKukU4(yrOYJR21?^1or^d)_Xy;6MY)X+nq#iw=~zk`<P35O_rtCX$za4>ln z>qna;pZQz9rFdVl`n7ucO3!PXt~1*ehORW<6s7xS^S9U2xU>D^)^XgkiRAuj^KE+i z;q_v+LV49+_!C_OK6Xwzt(tw+=-=0`tJ_S~ef8RQM*WJP##O(9_vqIT#{TWma+MaK zr8g<B?(Mcpo#~l)STu~Q-|lzLj=vl2OF~35)@)lfo!M?B<5jO0GX4KKjBLK$NItqB zbdI~t&nJ@;Tl7zUdD*prE0J&B^aZ@9<F{l4eym~fa=XN#y>`c=uEeKP!}XZ7bJ!0~ zNY6fg+9j8zkmG>>$HHCbgZ*tobA)fZg)X|7@rK)P5vPy9x`kGJzgtdUyz~9-8twD` zt<u}3|Mg6jaWZ*x{9{(g`GrDPi%#o?@9~pZ)_tAr{|>uL$J%X9tIhd##`ru-n}YPY zz2N%m@r2|#4C23S*>>Lg__Wl&)u=B~>UUeRgg2HcoCOU`!mbKsXF`AG<t)mX{dy&6 zhIGM!1soC|R_<7??sZ`KqcqE(EW$H180T{yd-VzwHJEiANYjPspcpOMzAF$c&#VT@ zmVD+9f(6f<+^v*wV7c}hy%5Yi4sDz@?h`S1aC!RftGz*>vRcA`k=ahPd<WzFt^~h( zqKwRTv5BP%!3JX`zASBEoWJkR+Po>Cq;{Zz6Xg8Eygx!%ia)L6kofSDYc+Q$$Y7{- zsP0&x&&u=T%hSBl#o(24Y(57X{Ey1!9aye3V{*Ccfd>EZh}ab%gF*hpTtZr_nQ&ma ze)($Yr675O1aNI$z-%XaaLyiWCbpVghh8~}Vzn6Lq8Hwb%yzXqqwi{f1|l07g+Vn= zyYz>Z9M7J_3K~4PEE*=h3S=;*#atQ-9O6N(i_>>sxoLvrd1f>)&hN6m)8OxFp#HwA zfpPx24Z8wB2BZ2IWHHD^zk&`l_{YBsGxq|?Cmdkm`4MvGAzO`BlVxQ%3(t>|)~l_f zQ4R`O)D`x%ngxeA)mv1j#~A(j`@a7D>9@s@v?C(H!pLN&zvqLK#ghrn$4ZKm11cb8 z*PqST+&(Q~t8rWOe9vOlgaheYRAT#ZC#t{t1z#?@w`&G3YXP0!|3c!;f88=}NHTh% z&)Bp}zxw9sLx)<q4^Q9sWvSs^3ue3IQ*S4Ko!!7VzwY+pJS=4{%o}%wXV}$N-Kc)Q zw_Dxd$z|L0>1)90%fXx_X-9kN{}1i<JmE2guIBTWy)@$eVY2iiBWs(u!2{zx6N6EM z0VOXUm~i0V@AvYcsoRCtdzOKXY+w@hN;ufW`mp-`?|U=WZ)TitsQ#Vvx95QdfBUz- z+p&xrf>NbOfJ5U(!3C4fUyII{4T~+k>R`$ME4#jIJHV~KM?kLT!@+{LTd((3KAoqL zd_di+Vx<E+6I+dyL$Mg{<POf>5Ml+W*&sAsSo?xT!hz)%4@aZ;1?qSVF@s-BY&CB8 zuc-1(XkeVrc<W}~D%??s(EV5?;XwMF2er5U2^c)MEdTrBTBtWcxek^WA?`wwSTG$_ zEPc9Hc(*F<K!gAJ_e$Xbcr`#wYUI>UIFNqmy_0!@4J*%&p5^<_uf^pbSd!q<Sa2YL zL*j#E&)+v;WuWMpo)Ei+7_V_%VdeR8MRU(0hf+pnyQ^pCI1}MSM%GoJ68hQGD_kZV z5+5=jd}t%et`f%s4gTT*u}>a=+&ZWB?KIql6T)i-aZGGA$Gq=7f6EOjYBx&X<sc@B zR7D+V@b`Z&7ryQ~C?0;v75&ACB1nj%_;|(-Cbk;2e|9C@#{v&DoVR%N`wAi<;AZi3 zfD+Wri+O3~pmO*4)uqIEZNULhI#lFduNe?|pus=YC><&NU~wE&wy_ITM>SdQWLnw4 zI6vg@YuxTa%J3I>8JX?w29(ZZR!%sOo=~<`8h5>fNN@*sfV#crRr7bqv+(@*ayYI; znwVgGk;}+zw|8%S%x*bxg9n!%Tc5a!r5A@`(gJ>vB?tXVgpc_jXgL4l=I=Aa_>Bit zRXX0hoR?0x)<Yz$1rACH2h<OKSj7~|%JU;cw780xbS1JvE8)QMRWmMYPvDUF;Ayaj zm{4qBg0%&#G?>_G*i!v>&n3oVJ@XqF=YQP%edce60}cN2Z(PhBQBo^3L7<5>!o&PE zEE=qd@!Bs?{<MGNx;^m@3(t=p>An7tngx*>u(1vt0OjTxv2WYToDVekn}@_MBdRo5 z!X#|=;PVk-|0TB{mJ;JUaBBG#l<?`v$;X>wy}>o|8Ay#xOjv@lg0Sm>hVw2rgV!x5 z%842a4mfa0e2_eG_nn(IQo_L&3@|Yt5k_V^;Z*;f^N4j6vu479bcOQW(!`YWAZrW) zS$KZ<OtF1hMN}cvz{JI~q=9jM<DIv8TZr|yi=e>+<G^oIpj`@(wfF<TU;-#%7j55t zkyw9^M#d12jGpiV&;K(sF#P{7B|l9ac9!s)yXAA@Lg(6lct7QgkCvv&!WI#xg`8T8 zxHUpqq*psBsd6kjv@W;&?XF!~uWyycio~{VZCJq3F?XMUL%q@lF%eBBPY)(V#l5~~ z|D4-tJlSW`%qO0o&iJ%d-rr;NedqUi)#qyOnV+=(xo6+~u4`M4`sJLSrhB_{_g!4K z_n{u!T+h)XWxC2=Zi~}_<^FeciP3c7JPS+BubbSTcOcHKL@^ZQU^!-QhXd&ed-Ly4 z!#Fb<MIG3z1@k#Metg+1{;Uw=fI6@|st&ek4GrfHRNdc)d6f^QT@Lob0v}$c>z{cn zgyj}6=#4bcvwc}Ocyt&U=Qn=cR~rqv=L*$gG~YJXfo>x_A2;K;D!Msv0}%{~2qq>w z=GV2qw_))Tn#qhze|;PdET6yEpda(54NSWXHn6bNuwDE8Eti<!`!%zn!QZ|*QEobB zXkmyXBye*4;92wi?QLR$?~5@b<NUgBhivrFPe#EKkOvM334D-@cz=64F~RpDor%fr z@0-@j7-C|k5p+rHhI`xdi3z^ZgpHQ`MiVxA9ve;AXt{2*#730tka}ehmDpcygX%|L zqUuM6#(SbGGJK=o>t&jv#W9vL8&p4di3)ts3_WE*l<Haqg$I|}UrbX$&r#^X2dWeU zRy8#EuM9edaXdC!`2?<9UNAE<&bM>et^(#k;s9g_R*M>gSXpYkrW*BuJb=wKWc?2E zpxc)jcc~Inn{rIiQg{%&YDytayOG`Z%faEma{ddto;bo5#c2yd7#ZgWE$vap>Lp}j zIRq3gu!C+xZ^-i`Dq5Cuar~H~l`4Z~2v&<4?F0lqyksu%CZ+`-pb_eDV0nn<V-%m@ z^6eMUJ?HWZq9+j*Ei0Lr?3Q{-y5lkpIY}+x2Q^k1?oJ{qT1JyJN<tV-(kPi|G)bdm zBAS=dw@w!RuYJC2?wrKax1*z#wX~GFYIoTrhNRB<_2-r@yA||eX5{RSQ6yZ*XJx6W zdU>@rIgIbe5*NQEE`E~VE$>{ORTr`(e(lTa{~og+)oj1=<<j;|JSb@&yQvQT+#El? zY$^Et=l+bX+g5Bb{;T?Q?S{{@-Df2o+%#>Qm)+~jNt*U07vioKUUGaIyL|c#9b#O$ zVP-?a`7{OF(jQ-~iw-O|P5Tpj_t-C6S>0`y7P-dGm$j&?s`S3|H6Xc1DopTe@9WF& zV@|3+YMq?*=tid2&o5Vgo9GjE{DSQ)EHzc0zb4(jzHF))TaDPdOuf^mUI)*5ex2XM zEj9JX!{6aY=g-gREalC7HASN8<UZ5FFLhc!r?B2qL5X-AwFBsSan&E*|H^#>Bh!yf zEVX<t?7!ZZaem0f<!iTxRdvYfJ`4^N-0z>eD{M`qyt{mi@axO#?f<T|yk@%8^75DC zauMS0K7l8fPmkN2N<^)ZDlYIr(>n9IaGfq|P0@pAz3xlJVx^hvGIIpq+}!zBxp;Qc z!AD;Uf7CT6-FdMpV&Wm^dLPC4TY1-N+J^|L%La*`pRe)c^4HmgL<~%9$Oc9GrQbha z<!U~dEM%8&lT{%2rE=9?*KqfJ%O4w-J*oJ1`RvCZf1Xv`d*Zn9|Gnl#3zu%?_<84l zL9&U-wzEHG9d18%u$uQ(#IboqRB;RDgBl)}x_`aOUEO`kHeF_K=7Hz8qKi}K-&wV~ zcDmNPocD#fr{40&1xd2MH`d_0y<Kfq)FO_bI`Nl9_Sf&w4E@x(VoguQ-|qIK{~oWd zekmQ@MvSd8OiXr919TGmy7SL6&c9~y`Gw-jKL2c+t!hgPE`8j(eT`rF@&<peA5T7u z&aEn&S`{;SS3a*~j3~RGgowM(>h5V@*IwMRPx;AZneTQH^1Qy8&!)&)y<U1a`H_U; ze4)6RibSNMSSN=A%csX3KB{?QGP89q<9rXs`OlQD7r)ij?rO3X-;?NLvV4yJsb?a; zr-}T_^i!OBC0F}zp6<7wRd(0s^j+3#3k^N8>DaZ1JsS+?#a@^ZRdV%M_jiBB`AIcg zKevQ`irltKv$aNQ(LTdPR(C%yOLjjpPkD9K%h^N)P(nHjOHG+uzV*Am>t}~At?)lG zQ(tyc-7+q_)zMuC&Tlne+Tj1|>pRKEx0vmun{6#GY*xH;S@X^0^Zjer_BgwTiZ;%l zEf*nNU$scF@5XwwtuIz-&s())eX#0$nNL6O&VRkwF7(q%znk;8{iGxpw=dflYlc$& zAU91B(b*Uq1rD+E;Xwz~pH@6NGyQ3U|B<GvE1Udpt=n0cxv~EJ%*WlYY;#4c&BETj zj5)eo_rv9|r|GXgoRSE?E_Pw(w|Su-Jyty^zO&Fd>HprWPew<#RjSTkDyuj@<mB?S zfYd#IMRy<laL~!_ZA=+45f}t^&guGlTQ+h0m{WS@!j(zdp&ueAYyDXA*VJ53^IZLp zIeX1mYOXyOi_u(qS@6Y4$GI}MGCf0Uq%OA_uB<w5H+R+hn5x&GD$-oN_xxLtXjr!W z)g$R#p-(SEPNu(HwQR@p#!^?+`L`TJKkBcS6g9mnx_3!}Br!qtR#f1_&A5%SGW$Ib zsGkiC`QWKJCp#}<&LQiyA3KZHmz2fiUad5H7kojz_w2!Ww^nsYL<)cWvg=yN&gDvN z+uDm}U4C%>e~PE!J6-kuo>eC67Pp*NQ=Bg%l$F();qiF-k_qSY|8LQg3R0cFuzB^P z`Ej;JY)^mZ5DYYj{o)b3%X06OUXR_FadFX#_3PhXd><ShF8=sfZ+CLPtyvpS=Ig90 zi@EtOoMf%3dU>?|=dqZB%T3Q&l|^ifzYuuvd3g7ymp{HltCey6*b@5TCC9HPU3QwY zgIQ`nJ?>k#w&S4Ygy6Zmvd_le5xKv+w(`ogkYii+W+%laXRlcsFVE|{_T}eaEBueY zF1B3ICG#h<ca}-skqM_(czoB3n{}&Y>F1w6Y}d=)^%k0@h_&cPu6nAx7A#z-xNh&a zTPGCf-@8+^BmLKxmlK)ow|(N~_%Y@FmeZ2^YYv}U6m<Tk>jCwof3HN=d8XHWIi241 z;Oz5zW;<6Lnta%9HcR!T!gJ3>rq*0K`e*0kUlXqMzP{If?daeBCHtrBWbog0ZQkRX z?;iWlv)p$2c^Tn8!wY++&1q-5+~sSx_{&xN|NFgP*w132|Fybx`dSYzd*}b~-X9fM za&Fd4CcB-g*PE{Du3X^9_2bW@mtR=ceq74^{jR`=lZo?WzTA{}<|$fptKXde*Pl=3 z+DS(?tdcO@WA%zH+$}dZb=9R{!Ko`-!;7WvOj0}b{oj}6pSw%r>p#ugSW@YIWqqW) zzh0it)w)YZOP(&;eC+F_7Q&TW<2)hIxhhqAbi<Et4i67Mz9MjO(%D(2u16Qd?0tTx z`25=)Iq`0cjPs3>@^V)xJ(#@7#JK;w?k=Tgr6C`F{%le@WBhPNq2Zognxc<ZYBX8! zzFwUa_;`Uy>g~egKV|Q(->J^pd1+Vr-aMy!N9_);>MmWkCppw)Sy<eEv)VJ)<n9w5 z2v{J`5mWVYX|zph-u%)thR2Wm`1rVdS8G__->n%J7iDB+b*&6u?v|RGy7AtfoyCv8 z-Old^-A~v3b?%KyM#lMb&2=){^#2)!e(=0na?Y}@LU7H@`+*0ar+>cuckRcQFTI0K z&Xf7znOa`-bKaF~?e|fOw8D;@sL1)dB;xk2$F;BWe*T>E$nt#E-y@GK)%fH?Y>xB4 ziwS;X|Krv~)mr{$f~|<g*zknU4gR^gxo2h=Di@ttzI^-B)6<WinD~j^_My&$%fhQ- z&-`qS*U>*$#!~ZaX@kGthnT;Y)s@q4+I{|Y^XS)iT3=jJeb-3Tcw7nZ_uL-(<dB`K z>%5@v|4WZ1yH0ri#@t7b$?o@(2LE{vR#}IOf3Ep(<<lS5=-I0nb>r@a)J$Jj7qaxh zCAr<aU9V34Va<u<G+8<?%YSW#j^10d9ae69>t1EeC?-^XBycyLWwtvmTOK1*aDXwo z=k`tEpZ~Y5_nF!3K0m8#qw=apdxYDL&UpB*zB)a2_wM)u%dKY`&HrD+_u~ndoqCVP zH_Hc?XVu02oopF6Q|oKZw=Fwk*X{6puzZoxwQzpl$F~j{uC<gh$%qKK($jP5)x*H4 zb>-Xl{@*Wu?)34S%Y8MkofWP5)cI$+?tA+$-xu%NUAp_+{Vxwb?Rc<i+lHWWbG}up zI{qHNdi3-%zW(p?wxsi{&D6er_3n?xl7AarzkWO?f3|bK-QIOSZoRk2tEqW1WiOjh z<9kBWP}YhYDi5STJ2TU;B;%aal>E~V=jGquru9rO@1DWS3%j~1UY~Y9GH-E9joE@J zADbKeB@MTmU433ZPy5Ff_L@s|_f~ytIRABX_W@sb&v|_g{{FJ_Yj0iI5_jat!>dO> z$5gs={a9kWSMq}=cis)Po>iUQ={{Z4edM-1yyNh<;OXA;$6WVE34HZk=^Oo9?ZIWm zM1PEKhXabpQjQ$C4?8~`;`ZLT_K9I^YqeFLoZ${5yP~Q}D~{4LnfF2SN8a^9cHcLr z9`(w%Ds<7BF`3D3XOsV}_}agZcQ*J{&s%P)6~w)Io{e4guYX5(O?Kna`WAj~<En~v z{QKAa5v{TNwQBF<7q#ZR?{uPX%WR&X+cE!7VdS5>d$W?>-f4Q(_gzM2H~;B{iyOD= z#$Okb-|_F{iL3pe%&Ia<bz(11D-!u2Sy3-nlZ?L>ebeLM&sMWd?A^)pHc1{_Kjx@C z*lhfM{qAbknq8AMKmU~ZaPr{X;$X{F=NRX2bMSZHu>AYCxy*JyciMgW^@+VkZ?>gs zT&2hL`ohn%rr-JLQ}b}za;}%b{Ia6=EwYPdUM>Io^4-k6R`=#c73s#@46%9hMZB-C ze6E3A-~aRNUCE5|WB<!mIT3K87n7~%hnrzLmMbaF`M4?Z<DZkJyM>MyKZ^u)14HY+ zE%(07Y`1%<X!PTgp=VuJuj!i>^OL_ST5*2JLG{0{UOx78?OxaM{{FsI95R1jOS?^X zRSI9Y^TDc{M`NP7e>`Edd{TUCyIk+@`8lhfe7>O0*PXca?)OKhAAh!&FufX?etg@r zuWJKicRv1JzGl|^<bbQ^&KJM?F4u1@7dvxbd1}qGQsKwE`Vyk&muxsMIRCfz#wiW{ z?K^CdYfvnO))b`+!i@8CTdO}6&#k+1!6GRuSF=Ck3?B!_k2egr`tE$1IbTON&-(ik zkq@4pQ`N(lip9E4Sgw5G^6uX;2i5J}<JoGAPFxQboqzuBov@?Nm;blWjWa2Hp|)r9 zbdw{g`{P9R*X4xjL|l~UJ-gP>&i3~d!>xt)CMG}L+Fll`xFUIy%!{30-j)1+y!z33 z?rUzfyZ(QuZ95Y9o3H<6dw(CNa`vNB)$@B-zmFG64$5A0`uyx$g|EeaN6W_v+W)@1 zsp7-R(vL?re>KeVD3#gs{DDQ$sSOpsH$E=c_%NB-&O7+_ruFkJE!BGSez*G_NawCn z#}WX@o$&=OjYaG=UYy_eyi;|4d?It}#Y>ljLPPJ0f7AG=Epe^2p}}8V!?sY1y{0I8 z-Dkg3jZ?18n9XeW(qj9IQ;xI0EDZ1O7K?ppx$pme?e3@cRZ;K5Kb$;g{wKBa&td*s zTK{T(e(C&pOuYBzf&YK++e*v-{<+6Pa&F%C<Zse);d?5xriWI}i@ht8|Nr5e6S*69 z-TLO?xG-D)hW)zVhcE5;@vU`Y|NGw`K5vtl<?K3tSD|Cg`|aJ^cU&ucbYXAXk?X<T z8)a;6&3k*TJ?seQWjB}7<yj6)cDI!?bO@w?60sX$2cEB7s%uzY7%XG5P|b~R?VFm{ zV)M6~-zoll*8KSD_`k1opA{xOXJx4=ySHJ9q!oKj(bIX0n*6H|sI%9`nJrwmq_V8! zLcW+u4u`<guWHK;yzBb*@%?xraDM;4oVbr$i@weg=9dVp+4pq4_M^4oc7ZkL4$i(D zXJHk3Pg3e}Q1(Xk>1M~i2Vb3ge=B?Wz1l-^9}kAV*~#h4$2#}V)%Hgd(_eLeK7TvA zV9vLui-iHJc5GH*tuKn+5VJG=d&t)3teN-jM6KD~;LoS<R3YeIoK0rNoUf1WCg0kz zD`G}e=F?Sn_Fh-(Kbe*NSn%WPZ@xZ7)9C-~8zlBjm1nYxK3Ld^=<r|-scl*twl<t! z!~Xj5Gy65V@7DW#^*CppSmAS9yOiNolD6ud&$pD@j(GF0(boF@q~QFYpPxY&Hor+S zTV59Ca3DRM)qBtX%h$JueyIGc?Iw0`)=ur&m+eH~dG0TAoj;H7$DgXYziO|2r2^+p z@6`@sogZ`P`QqjIYvu$$3V&4;!FMXA;Od)4w^EIlAJOK&S;f8fdz`uXy1%v2zc*Fe zf7x@Rc>A_`i*rBUJuZBHKkV2Ak!e3~-sbOMK9(=laEfuB@&3Qtf4mi5og9&y`seH8 z^XFYBnB`QSvnjvlXPXmJlltq(inaGlDvIyVxvJ@6Vm)oCSY+jEtp}5xT`ueYjxPNB zXYsLP>hac!pX`g~Zl7<zLFZoO*Ex@_{Pw$L{O{=zTdVkc@6I#X-S&OPhQH_($Pj;_ z_uzB;&ECSZ`DSk$f~WqI{`*I`{rDAIGv1$XGIprjH#PX5{}U+k;icqt-L$(#4<<*I zEREmk8h&w=HfwFw(;c;QHQ$QXtlP2nJ(Jzeu5&N%S8d5SXj=OH){L+Bn$35djlU=H zI=Cy{!#DJeolfh-T{nuJ&iNkyKc(>L|Jnx=Zls6!ezbf#_0bD`TZzxpO^@X@uYNR- zUv83<L+tk-zw@$ofBhJLM*rPwtp~-g%;h@m&&5ms?p@nG`+jZ0m5}^t?=tJxaR;w> zc}yhlRO6E0_Mi6TUC)-^cXe*V`LN0u0y9sIYdK29Km7bsdOErE?=RhVYyAGt{r4>7 z<Z|UFn{6M?v$@G@ekUO0-OCvf2bMc8+EdLq|5H_&_3ipUJJ$AIu3XvVzix(cdgv;t z$xRx$sp+5Ew|sr{@yRzXyXp70EsFS+yf@^adVBu7$g3eCZ+E6#I}~wy$G#OE#dljT zcYO;#dU(o@$M24Z9ld$Ka*ago+1uax7k`rttGPF?!RKH5%Z~GZRi~}Xytk<7{HwNU z7B3dDcUjt(e2V+H?(y;R-zodObocx--1_O%ZT)+N4to*~O~^Z*_xjK`oB!LM70o4( zaoDCglrzphAGhBoY#k^<ON&lCU#l-KSlz#}3)-Z5-Ew*A9Zh{1q2uzKHgfxyzb-9Z zUu><X@L==fvfHKUf*(8|PW$Zlj^oD?E<4S)mjVwyzfo)U@8SJ*o!&Av-~Y7vUYk8B zI&taO?gsy=b^X5j_1)_NomVZ+yRb|&f7<%o&Qf3L{|8UxZnc|mD{AlWmbKfrW_ez% zDKf}-x@%fQ<o$2I{|B)y@J_Q0<|yI&ac6c^{@l9E=;*cH=Vr*Pul~1Ut=3r&-5u{d z*X}S||KRiU<=2j_)~{T$<GcO8IwzTZ=P%?vpSgL7;=POamaU#*x_hohtiXqxv-+|L zw5m6lHojvr=bmPEeChTzUB{k@2)#&>o?><D&Bo}^Gtrl7cRXKmnC<xK%kR%cm%lER zd;IwvH^+}RdlEg3YhLXFb<Xe20cC-+bEdwnS1kIcD*L_U!1HyLUvtmj+;?wE$U*i0 zQ};f)CYpI)>c3g-_ieAOe|}zeW!bByE9+KG(cS*^*Q1w{cdfni`AXNZAMbaI9ep44 z<Jrgb*Sr6^tF6Ck`Jnjhf8EaSe|M}cz3%_NFltXm()WXNYVze~zrSho;Bz<ov|Nt2 zPYRdjz4vciT5nhQx^d~+v#Jj^o624%P`M^#GevXXIa9sO@}GL>i<f#&HgBygzB(^v zZ~Eul?AN(=*FN@v{JG<9+1;In4~$o>SYn)>y6VB?y3&vjD?7U4Z10(eemE)Ga*N4s zB9q<I^$(06f4Eq9;D_(z>Zwx&C%a!i)>zF0YBJoino_DbGdk|?Wz%A_pYEv{zNtM@ zQL@VH;YT}mt$K9p@5(O!dsgf<b@Q4m-{-7f_IdG@U9Vq!yA~U(6FjHtdjGQ{=l^ZV z>25f`b$`6n`@MO2hH3KyO;<_<R{ap#W*ldt_V3}0Le-u(RUHlf*Y(a3n6f;ujdh#o zj++xF_?JJN_s8URRQ_%M92ZT{Jfp_c#52p+)ta$ZEL!nk^77et0}n3eDV}cQTGtSB zP(62Nlm9)v^P)9D7oI;hJ{5oP`KFn{kEXUSnU->Ur*6bO3H8`N0c-Li;#(^1Dn7*( zesbJ>^oQWewGqqT!t%StWy=Sj4>WE4IBWg(t=E2C$`pRwqr6Qn&g!dl{}K7;>+dQ( zD2~tiS{hZodj0A1v!&GK3>IB}W&1tw!1CugXMIpQ8t93-@%b(X&BobpIe+f?a$r}* zTfykCcP|TcQup?4{07g#&yO{nUr`dDE%@Q&+Fe~-b}xVYsC~69d*55xn)IxTS1b3{ zS~J;g-?H_^t*K0QbG4t>{GL<$_(^m5>rbzrt+L+v%MTP!VQaEGpVz)>eDnv@XkUM3 zMo7)OSI0N6p8fcRzMaJDS=aIfKb*WC_hY8L`1aZ#Q}#ZZd%X93R@bKa!XGrPS90Q? zVK+!%@#4MnWWxE>m9gQ$#p}c_eciUUz<=NBD_?iT9lQyd(+ABK)%ixV&j0K$85#0v z=H5j+bC1OYs{f6(tV=r2R+DBMx&LI<?{{IbEOmPOODi}2JAQx1>yQsW@0|5Ndi{OP zvWk25wO=ks%)M1qy7lkt^Usccum7G?xU%?w`g{x9(0e=0Lm#YMx2D7TeszG4Pwv58 zxBmSN=YF*5zK)jO+lg8%cfUV;o@?~x==|T_Ix#mz*q2UAGT-;QVTIS{(ygZ3w;cN< zqH`)bPd+U8?$;gb-rfB(r{VlFr9S)~cUTb8_?Gj|naC?L_jm4nJ>mSs%UO?0^(A+m zP2HZpH`g!CVLKC(-Q7!5z8^Z);2+I8Up=UMU0;_~u6)qBWqTL-=gYj`eS5(a?X2F@ z`F@P^x4iXYoWJ^At(o1Q%auoqORr05x0j#$G5`3Y%G&5j1)m$v*A`ztl6k*s%Z%jx z8%saFHGeDp``tdnCzIFiFT8I2@o4n?zG>eqcX9oQsr^6c$APc@VxMc?gjOH7moZ-V z<IYd_V6N|T>W>P8nmKN{rKxv5A9POoo*3Ic$?E>~yXlPc-=)mKxXl!~ZQsDqn9G?X zw_~PdaT<^8t=fR)t0(xcd^pcE``VGv@Yt;<m18R&w$7HZ*643&@UN8FZh6{uWs^VO zyE*?)1-jO$v)BA8IxSjL^mM`dyOXqkY&oWWP9l2R@trRZJ}o>Ly19F&9=Hp7;>6o| z-!~rp`mw89Uhj~#rN-yJD^jnQFqe0GuH-tuCzW?%bVcpwb*_eXp?4~aGMx?oKVzI< z^J)J2nbOz)7iJdLX85N5xjQAX@Wj04<if1IUw>C-Kf0Ly>hYDsZmxQ#Cf}|t75#AX z=KSD&-wJH@6vP^ypXuJy`MF<8`tq@>qM!dLp8eeXdz#3@c{UGn^<~B9SDd|f^oHzT z#Rr=g7ri5}$^&Jrr*-CBt)DTL*`aH4b9$5cR&|H(&o~iwyga7n$wc=@=d9nqvC#R( zWH-BD%3Fs6%X6kg&wsH<{%Kf{<pbkgE9QK5$+|dolk&xjCtkM5<y|aw2j})!%LkKH z_5Xfd^y3OI_ow5$(WRlTPr@Q9trKr`{P>_*E&6bF&7IH#>ftYc-dfZ8@#*b(eczO9 zD_^v|Ej=$UDt>-ONXUzm3m2|qva2lM6Wsf^z4%chYqt5F+0mt?A^%=l%zXZPy-9o? z_cr6%4d>6j*hygZDRVT^2wfYGrtX&DRdMU?72hqr?&|Cuo!Hm+o2AA|<<xHR51Q-K zw{Ncf<lf!gW%+!+*47tZ2PYpdW}V-6#P`;Gi<RqcSD!9?_)X-)%RTq5OuXu<y6|*< zT-Y1?%4h3>4=#WF``(OIjJ4mEEl>J3srSExk56cgpH+d&ld{Oq(@c*Q{{MMBsUn<l zeqZIkUtf0ZNdNZg$LZkg<j}*vDo!q0SeUZz+2-pL7FC_EUOziJg!Pr)gUQcN9$ciq zbisx5@)2U6r<;qV$$pmK_I<*l((Bp$xvSZ(6xO+aXR@=_dTvLcU4Fyn!DLr9emR+) zlg{rbe7tB~-t$kKU%!3@O%^OFI=`zl_ilmJYVIE;*EH^@GtU1MnOC~3mvR0@&5X;p zG#^Y}wf9Mo@y-|Cf<Io)eJu0g<m=k^&z@%fyrvO;AbmB5b;ZY<SAw$rRxR3m{=}?B zy4U&qF2Bvq-}$I>$Jd9V8}HxPwd1iZd#Uxh$A?z^npTk#Sj*#`yB{<vmbf)kcKRCv z1z3Z_e6E<vr&BkUy*<Wfy7#BSPOl4=&*xm8?aeo1D|kqz{?n_Yt`nZ=u0N%_D~NUe zl|`%dxBo5k7OlC&T=RZe$U*hD`dY!P^Y{Jnn|r-3<KeD|ZQGWvxG;J3*OKi|&7$kS zx3VjTpFOF&P0up9sN8ujf%1isY4@H3Ya%y4y67(dcJlK%X1P%^^?yFrZu(YS8^}06 zuIjKflihkh#`#lLynNbZdv{$pOU<RvF(qM@Q6*o6o|{eod23DjBkt`sp*_2Eo=vHE zs;Ins&-2UXdL8-q_O96R?dAUa>gQ~WzUv*gB)GQgSF=Oj?5~gI|L?fI`p1)3w#I2^ zI&R&%_3yxy_wxb|ET0~CQ@w7-iWO5PYyWuTb-O%2W}4h`Q`VYeZ@v1rUOvCcAoNM) z(WCrFKb7hR#zqP*moqcsyPux1N8#@|-ILFBGJD?t&nvuRUhcawY~82guhHMTSD9RM z!`oh9WMcWX*&**@>G!+k#}Bn~FJ8ECq5FO>zAtY!9+xXTV|d(!@8^vwH-`hu(_^;J z_$q9llYKEXaO1)<e?gO*p?&kS8R!3DlB=nTJ@e;W^2U_2*KRM|oAPPHk7tpSliz%B zjNW5TXmkhUyjooa(6r2h$MzZ7%vZfn=YBW(U-zweb(25)fmz4C7QX$j+xhzc4xMMZ z*&2uJnpbHhJz8;VhS4&!dsCu{<{IR>951LQkfjtH8V=~GD?HeIdan@wmB~!Lx@%oe zuUj-B$X_odkFZ{cnJg?dr&uzj2rFe^WU^IM08J%4BhUkd3~U6bf#!1@XAv`F-{9a6 zS}4%6%92nn1or}`csd+d?lWN-;aNP;IznM4COg}OS$;%$F;z(5gQoD3Oak2tQ0%gB zd}(QD@b_OaYckkXsP#W25Rit?M}rz|-hVWxQIpLeOVwqD&+G5+)QbYGS2K&#xn7t( zPeUtde&E!?SD&-Z^&SXgq;OR4fU<+besO^hFPDb@=#6}R$t%}!PSLyKZR+ebT9?*e z`;z_tWAmda&RJD2qa%qaBo-_Ot$>T#QTs3dOjLBphnfE-eG0qr^Q^M(#)L@kK*<#v zPbOcp&RGA-;^l%*>n?ly5HZWoA#lUT;Xu0agn3qfe#u%sxGZJ-Z(Uxp?EG)jVx?dG zdUN38<<rrR*0oC|U)j*LB0pw+-E%KHLH#))!pHrBPcC16Z+RsVCDvY5P(5&I`kj4d zYquCa*j(US92pij|B}uAx&?+>+RA+Q_iO$8<Kn$0+aoyKWlzfeSr%3HQ)^B!<q8ol zv>fC?Tg`<2sQ%x*Y{eSm$0v5p`P{ku`mzT9cPm3DPO{kf#P8PI7a!jT&t4~*eeL)~ z_UlJ}eR=I)pWRpLTe+_znSV{U^0F0AE_<)PX@uVG#*E^|O`zoubGN+a+&_)U&a&Y1 zvBg2%>$c=`|5&oobwm2|Z~bR-yI8xgAAk6px2tgR>Lgb#{=bH;HP_6xEc+ASI;X@V z)~0^Z1pn*#$07;)?Z7&44f_81>g`KG3%2%rTyfC#!t<5i#A0QC&g(mVW8&kR{^rLY zf1drgax(kTf7Se5ZPTtPJt_WwxcB0P8)lEQ`Q)Y8-<z&TmL#GgWoWEZ0!5VmtJT}r z>S=wSQ}|^W<NWOWvr}t>Jm*h2oca33)sG9mJiayMwxrDxBffj-0W#b2eNDEy2!4v( zw@kC{e?jo7C&Gb|k{^GG^LPI}9{&B3`8J|fV_Jf$;e`>0WW?{!Vzygb`0S!{knH?x z6<NMs50^a7&X1h?4%C$UaPqmf)z>##Uw5dL-j_1kp~W}HK%;L)n7Hoi=*5|}?kAR8 zzN_6}CpGKR8BObwYu@e0j~F`HsqQgyCZayu4XVxb_qKHhPf%wsgA9C~bzS%TR&<!i z)M~wo!!t7cEay)-tNBe=^UviuPF5>#hyA)6_2%=}nst`4%VT*~uj)wcUAwU&G1+GK z0>iB@R`rU%pU-4>^oPi&%=J$;=adF>{WNi@O?D~W_1yP(Ux%%G*w;(u+ldLF1Lrt6 ze!S_qS9a%jy!m>skMlZ=<E>Qw`iRzqZ5L^rpPlc;IRDG*yT-@1ve%fgRTnKtciyo) z_=fs?`RHiL_P$kGO!nrs8`S=NadDQ}7@wPUF*MvJv@m|Tm!0L4&%5ofyVtII5<GX4 zt@s=hBR78E+TDZ~J25aa?cUPR;IF@T-ufjC{--`3nW=x8aefEu>R{ISTVjhJUrPFa z&-l3b)#_VX-*VR6xxBN>Kl;J)wWrQsc{tT@-8$U`#cyp_KbjbF@c9ns_9OqQuRclb z%KhwR=WXp|w{nvI*#)O6eruO?KWuKTxwZ2RF%h@~?3`);_hhCDez1H!W8q5G@Kp~s ztA&2>{GFW_6Ex@l2g~YQjvs5z>+T5hTCRLya<i4?mdle?{V-X^ogDOaUaeK={heQ~ zJ^gs5Yg)zMz{5#z@~$2=zoqiz^2$l)FNOLRo@ajD<z=_ES?f`JoUvBcD(x!IgG7`X zj7+z+6&`Hfn`CWKH@U%o*5VZpCI?wuzq7%jwLI#v@N++pw>xgF`kZrT`2v5b+0C|F zLq!cYsXcmGx^`uezbj8J|5KCY2j~AkIVtH*w4a=0Xl9H%=X_r$J57}<SA;H3IHvEZ zIRE~?%vh5pUUtsx;YaQFR;BQq`hJsOpf%R%Zzy|vYsc$#yOS<1at(}+zrXnY@^$O< zj`d25AD64n;gP&_?dl45ahU~^xqp1Q)cyZc@6IOw>^Y@xHYDv|u(;{`da<XMKfc`V z^G4)D=IRHR1;3mWtqn3;&hhKXaoN~t!RBCv<yNKFXYJmhxv%{9rxj~g_GDIHKeFrC zwTS5bc2cvVE<gVoIKS`u^CAaP%O97eOf&CvC{7KW_%3#j$yQG9=bt}T$64>1u0q7> z>n~FroSmIxs^4y%=xn!l=hK4oUtV5TX0OY63R+;iKXbZq-OskEu1n`_?r!iu^5?2% z%;a-_UQ9nPa&Y$fy}3n!t?KQyW}M%aJf3r2Q|srF?w`fSzbdYjzP^`#ZTD|^&w9;b z#(5=cZI5rhdwjl4anAJfZwc>Ix=_!Oc6OF%zum8l`8FRw=MbsSDR7#<=5I`V$bse4 z?|qnFw{_8rIi|*}HAUX*SXV~}InEXN@bl>9mz+_Ly~W?{R(vq|u#M%5&4y<tYyH?F zpD*|2=hOVKBOQsMhFKM*S9sU;+`4rtWa)Axtzho;&rEix`c8TO_oe@H@z?wQJ+V!G z`DxmU_|10nV(!dX^><14%TuoDy{}a{PlFmK44AbWl8CKp!BNrhFBQ@2`qJ00Th|x3 z*zM>n)9kJ;$DLK@cRrtYyYS||9wsKc)FXFpg}5G2Pt8n|n;%``dgk?t2cJK)y3R;H zWcWC#qBL0RNN^xaq13hTql=C?W}e#ixbL&|uJ~eqE}^BR=d13v?CGv;3l)DIQ+a$< zhR@nP|8jrLSZlkF(6o+2gM+=mjxU$Iw^y9HWA}PSa$o1i$H(87aId}dEBoRi*Na!K zhy*Y9>pgYqRMOs^#m|qu-F{#0`TY8SVy~??er95_v(AsW#1sDqH0QDE<(%R_ACw}D z_b+ZbfBxC>-_egRU!J~XlC9-~$*0~u`DDBDdf2_KE<b7<onU7Hgs$nB`0>Us&yCwk zkN>`M_tR&~qs8;S{^~qh>?32hqM}d!&dy~w>OX8%_WmWuMtJwz?sW&AG0wkr>(&gz zWVa_1{QdJ!PuK6CsQi?_>QLl?<?123XME<~9}zd_Eys^D-i-6-JlOebxxd@FO|{Ry zZti}4H}pm4saX+*KPIeLFE=@F^@-Nnt}dIU@BY8;I^Lyl{$~D+7?zrEo{aNt4u+Pm z(|h*w!OEvUxwn~xGDq#%wep8v?B5k$2bbEGNsC;a`jh*HnRvwHKksI5w)(eig<YC{ zX2-_EY)Ah+e)Z^t=HaC8A*Ck?jy-Hs+faGn`HshZ-OPLz3EA7<T-ds7+O-`<seE#^ zt2B@Gt?2sfCn1*H7XJ1^Ox~?+4GrhF^`0_%^x5*kWbGew3>MvZ-r%48tMse?Gndp= zS3cjo{H$!}Ikm?5U8f@AZB2T!xsPvomT_T0Vql=Lk=E*V_t0N=?|%FDzs^EmK7GE~ z(g^eL4=2?>#c$j5|HA$LSMRR9wW`0|eEM<I>RX4r?%uUnv1$>=+C4e1K2*H-tUh*O z*0!S9yS?%n&+ETVfBNxc{JH*C?d?1sysOuCeV@C<eb3Jo8xI9CKjm+EkAEbnQC8rF z%>(1&XJ;g=OwxHzZGT#DetUkjXtB6?{*020Sl2yYpDHJv4|KAV4mkD0@xbz)8PT&} zeYQU@T62rpF4I0gwEDpFUpHMJEL*JN@4jI9^1kzSxmi*ANsm6n@T#%ayz^k3zvcbe z2LGsn!)A#gzA-k*L3>g@ZQy;*_Ne{;jzY(p*Y8}9hM&LMyR2_*FK9W>$<*GDM>g|b z++??ZYscj{11r6{$4ee<mM7HCO(+J9PK(D?JbdO{_G-uK0{`PJ%534s-2L?Z*%$U% zzP%0Ff%4M-(DS-&Ya(9ys`q)Y76%<r5C7o#d-eKy8}SR7=Ra?8bInS;I{A3I|NZ?D z<(E1fkN!Jl9VUAI>%;3)_cxt)kKbA@v-kb7gL6}}XD63&t@-n$|Ma8ppRb8z_shhH zmhXA~fbn+IE70uPnfJ@YvR<A)*sadnt3TIH^V{9M+cuoE|M%(2t{=;@pNZt|{`9fl zZ@!2zpZr|=TZAgT18PjRk{@nH?NGNr(xIsNpm<ipc{TkdKI}|(sZDpTnOHuU%q;VK zSy9MbX1m<Rd3_1~@84Rp*L*6jee(4w|Bo26Vy``)CanAa=$Wbh&d)P{9Mbg{Em<yS zrM0i{+LK&EJN5PZ|2%33O(P`7WX&!+Ud`7fSQ<S|yZX<~Ulu=C^SdV(XM@&H?f7|@ z-A-M%Mw384;D%-6Jgy%(+INB{%Kh${&fERmf8EWU`VZdev9i?cO40m(^8CM&%<2Q_ z+f6F7ZFa8OnKAY1#Yx)#9sOrcf3X&A9o(s4!Dq|ZrypkxKdZ;EUF^e2=AskNx8&PO zeYd|EcJlc`e;KjES=)KGi%j0yfBt{Mm5}tcvbSdiuU>tJDLZ}pADQM=Wp(qm_P&0+ zp!=W5PUh>s{cjon`+3B6SM8S1di6GW^(m<z|M1s;+OT5nx;JHOxCx{*0|VA7r3ahW z7WhxI`@3Rc;gKu7ceB`*)t#@+W}H87^WBtZ=l`T+7S_D;)O;{`lCJ+cFWp_2*7r8f zyE1FFcz-eL{68|ak{?bg#QTTV*p(MYi_V|_@9~z3i$@mD-f=eWlEC!8Re^u3wr-5t zuxR6o1Y^QW;#M^j|FCm@w4%2C_?IisvJRb?wd2#h^vAD$Pd_qa;nl*QmBH3|ca!R0 zAJ%@9ULPgKzie8h|B^1N#cd(tS4;Zl80p-rd-Fq<>yOPOt?zq2zuM+-MLo_~vF~R7 z53$!}bAD`(KhyvIZ&_jCkN<WB9&e1jUTr)~a3|3%mQ%7HG*$0>-Zh=4S9$qWH*at6 zkkHcj8;7lL6*4l;m)(AQoptd6_3h8B-Y=2y$n4!|`CxKiarZizEuQjKUw33&oI3CE z@Bh0uMf_X5{A(TSe4dKK@zajm|5>2>;M3E~>c^zxP12fQgJwx<-aP-@^~y}zFYlWo zd(yg!&sX$Tf3n%V%ktj8!#4}hzqva5?$xp#?p$24)d&9--j$zr?C5s=o|>Bb?en)E zy>UnEb+EbK4S{txlN_uHU&J{-dcE>!1fSg3D`l`{KA|g_E+&iH+OIkt;J9M>ysLW% zB#;Z{Ot-my*lgXR`b;mct6A;JkrNA}-^*;?`*7au{C$>je_w@vKPz6}w;r?sKz~Q^ z0rl`#np&XM!9^FIrwYHS`r=u)c|oFcgT{Ju%ht$G#a~JfsJB=A`<ni_>;1gd&*xMe z=y>#5w?0-dfA6QxN5y>i=6pMJapk9T7bK=fb=&UTVz_Vqs&{|Z+3xuKPWW2~|GuR2 zzJH2Nub#aBSHKf4%gX9|am|mni1+k0f0L2%4*H<Jo%Qt8>N5Y(>bMEd&aOSWi~GkD zh9Ewrr*Dr=ZaWsKf6sPV@9OSW?b%A<Uw;O!`Zsy|#>4DK*WWKM;8}U|mwm(02XFsu zOz|z2`8U&EJaPGSUs*Z(n%8x=Et?+_2q^;rmTh7mW`0emz4NDiE35M=UBTyb4Q!UZ zoodBUx^b$|yUMqM&X1O|UlW~LU9=<p-=Cj_4_n2{3}**hgZd_$n^f=pfBE{h){i|u zMU}e~&fb}NHgi7L_a*T=7y0|M*VNhWubcc;t#`$_)5k<tHu>v4{Cx4UdDuD4kJ_b{ zI_#->J65e#I-g3Pm2+!z^^SpNW)&B`oc?$1j=Fz=$&V+mpDXZFe&@N{=j9`&-O4$u z>9_a&65Ge|es>mse!8jVUw??>i|CbCBkozTFFa+m)003o@xW5~LhOO({F|j8&$^#; zi*dQuU-Mr-)%p5YR_924x_Pm{-%aGh%bE6GOm?=9cO6|5%~liCz3z+l_7_@FrArt1 zub0`r-gxQWrt_lPzs4R=55H*nH@aUeG;`UzIO)yzsz0!8|9M(Exz_6=&&riuciWd5 z1c^tloAdeK>-jqC{_N+M5&GS;O4Dxno*<dOOWBkEZ8~-%w;&WW4<E#_;<a4;{l9M? z{rI8(O#FMP=!cgX-CIHPdq3~8yZL@idpYmh^IZn5v+{f%E|c-t^<Vk?>$1Jy9^T%1 z`fAU;va7<b+FP&N)`!Ft9eeV2*Zl+n)sKV)(_PjYyV|>-B4YQ~ad*7>a{pz%-_#l_ zC%fCi?@ms>TX2}y_36C6PFCdE+rP*AZ{M<*J6-F?lO@OIgs#l;;aaq1>fDp^m#%dm zpL|>UM@-%AuB4a!UxQfZ%j~O63k?q3`g~F(cXICIUyGE^m5Tdm--;LI4}R3YTRvJe zz5e;eq%WrVx9$6Gl}CL1IrECu>yGn(m;U(E_xRXxcfXsfZ%cl*_o;ky=?YW!=LY!d zx@!39I_vZ6gSgk%`>fsb>05WXjDY?OgC(~&-Y$OF|M~hc>0s7{rM|C|Kh3zc<6|uQ z6jPfmw&#-wE#-al3mh=je?LX+EIZxc|If|x$>(d+`#bg?4><~H_iZmd^zzm1d7lm` z7XDF`-u<CudtCT;tL=|L{g1ruvE^cn^F{J3-#_VV@L%;{@+Cc4(VD)y>5t|Ay`8n= z$iedS`8QJbYVH&bi`vieW6AShH@<w@{zA%hiPbB%+^COfKW|M_T65{+jh$<+u?I!3 zE}y^Q>$Wi6m9zZ$*w)5e7t(%xyY7GdDu$AiYtp+`TCvx>^If~$Z~vCHYs1!+O<sL3 zCwKNlCX??=!eVB#*Zli)<yOU4)qauf|Bm1H?>PKj^443E;%O#FtZ&^enYt-$yKw0) z1M9Ot+w$$Cyn}vZoVoDo$4h%ZZ6~|AZ9hC9(?Pg9m1cr3^a?g7WERI%onDoZ<EPlD zx6N_o;<IwQLyzuJz1v>xXO&a1$L8<5J*C^@o*UY|-_y`={+#qQlf%o`offT$dVE&% z!Q|C9&P3k)C$987{!*bod(FF9;rGAIdHdSeQ?%w&{7vn=*ooiPe$2mIp1*tFWcz%t z+*NuWZ&!KUEQ(&IKhJLE?Nhe?YcqQO@5>JSqt}`*SHErF?fR5YQFEE?uK&3|sp5YB z?Wppn0cX0^%H+?S{Qhj?&v|E=?e<qB+-$qxCl~(e+dq+rU(bcN%SGI}7Q-DZy2yOV zx73|yBl3QizGeM=d|~zU<ysfET`MEBPV?9L1J{_$KhMkU`|l}QwTt^_&fDnct88~x zoqu-w+O^v?YaYvr3w*d)xclv{;^YJAA%ULh=T3pQG^}_KEGW9SYES;E2a~lqwzAeJ zvDTc5KbU^(;o`>)A7`t5*V0y2>tEN){9Qsu*zMYuQjOP6M%(xNTAuwZ=TpBI->x37 zsX1<Z>$(I>LyvC#9V|M3PbuGzKQ^qz_io1dK6hVPdi~<twYzskEVKB!PX27?{J)tu z#Te&j*Y7vESAF+R(iywOSwW_YzI;&4N#B#<^XIVPV=sxDUne|W>2>ktbUUejyI^@! zO~uNemBEiwgWJn(Z$uI(S`;pLCPX*RmgW3eBL42=>AdW{e*MoL`+<f!S?+zi^J``H zd|$?S8d^V|{7x$m`EYZdXIWX!KrGjj*<|^#L2z!^`<eaO($|mAykDpD&miCTRbhnS z|KfA|K7NXe*q*+xH<G|y$b#*xUYvJw{7*bz%6{!=cK?k{MgGUXYf-q0&zd#H&#q#- z-u}FGyC$!G|730PfpphZ4WIY;+Xl>;|L@Snk5eTlcb$*@TTxb8Ec5p$dvfgMU%E_o zKW}l{e}4XG8~^-G;d?&s_!#ROxUJY%cWvRlr~U4HdZFQAN6gR7l39PNu_5H9+zA%~ z?T-uZ2g%yLla4dVTH3d~z%Os@)a`fdbhcl+H7~dJm+^;ltJ9C`|KDTtNhrSd@so<X zpNl2V5FGsQ62CF;z}Y#jXFtxpy_@f+jOALhxbKq|-j1oLzIOXr3R{V`!h_4YzLokp zJfKa!l}cM*yjrp1@;Ak&IYB3s!@s=jeOy~EJ*CKXZQ)N)xwkD=^Fi^|z2}d$*ITWx zSoba}`RRvqlK($Pm^@GMxn;)x1GF?`&bPJh-Pxc;^!fWU@82l@-m5r2=Wp-sd7b+A zewA$5a{glIf#<J&Pd~!Gu71;u=gI4DS^s-DqwwhSbM{3~=YC#aq4e(0?Nck(uIs6` ztMGVZx8`*~(DUC`_2=%J*T4VsYk$qJB{K?tyf`<#Rt;%RAL|gJD&B>E#nf2%x06|S znOt;?{Q4a~9xCmb+-&sTW9>#itAgz_rmXsKCi<Gd&erSZ@1sN>2OpiQKHW?tJ6G<* zN%j9fXYDNfD?eTAdi%AIWA}X}#oFIkT8J&b7QKJ|b~U-$Wp;kHIU#|D>f3+6(t0p? zv;Fd#w+|~S9_~t-XWTC#{9NADY+3JW?PouoXFX^CuB&;-w(`L3I4eE7kF)l6Z(REC z#~)q)qpF*4X^HKv__V=7d+o-xZx7hMdURp$uJ?sI*8Ywqu%xN+`7+Qj;Nw1PJwA&E z4QuOe-ds89{M5~1M{gawX7Oynu8ObSUcdjpaF?r8dAH~DIp1euA8xiR&H)WvEWY#0 zMf|VE_DQE0=U;SvItR3Vbc$?e$cCji{9Zg1i!EOw7CZO)?WxRml20zh>v#Fz-LQan z`QLP6*97WrKd`If*pfv~{(QWkmUnY;$BVc2bA<VIB6dH#cW2e=#h?6-pR~DOzw~EO z^oF4Amflj2X6XOldgY<?vAwT<eEhg@^7mT>Z<vurn6Z{8Czxil*JNd9Cm-vPG)(cx zTsSvKwD{}1*XwrcNlD$}DlWOB{@}7=;Jv*K{*|`7=f0h69~BdF#`1sYO4o3%c{cO3 zf0U@?UHW&#v-Ix0=sxc4%NzWke_htMMo)_~w(j$$r2i+4%ja_0$=?5dEQI&PW^-H7 z&A}YYl4s9Y^<z%SgN_q#H$BtM?s@!gv#!kL&;!dK&)aXeR}ZwA@!zK_w<4}?u!}Rw z%#fM6`sWfZ3(M}$_g+5}3vC2-@+ylqyzti4`0|;3+R;Bxds`!q|7m|~yCmc8*IUlF zS232((NCSfT6FG*sqJ2pyG}|l&iA#D!QEg2cTOC3gOWo{)rSWMCu;xfkv7+R^{Py7 zen5P@e7Bgcme%(*(buo>E;ruS;Gf?6saEWR=KQB&>9-_4NX~6CTRWG@PBU?m_t__l z^j#k;FYhZppuYb5-r3WaKaE}-!#IDf(whfI(}TU&TCVImXQ!OqwJpZl!gu@jy-&N; zA6;t?KT?<c`cdC^nJaI1^l`6#75!+U>sLdat$$}3tWs;c8de&!i9l**WUAFFcse!w z*sZP7)>pnfJN!h;@%fx$zkEH*glj^eRS|zrt`-+}oV9(A*7g@(qM+{6zB}J;>xO>t z<o~dDb`b0QE&Esah}Qi2WL+BfdtvMAq?@;<EpqZ-ckS}sX}5F!-W8YYNe?q~&D&Ud zQgZTc=~p&{64#BVtccl|=UXx_Kl<^w|NZ2x^Ivy!{rGZ8`kugt%2KW$OI&>Q#oxRv z+pF|o^1SDZHlMn@X!Uf9?K{t0PqP({-|=*1(j7=YSMugl+xhzn9cw<nb3MA%|NmdR z!gmvQms_6mB-rqgXDPb9M*jbg<9p+ds)xUyX_)MG<=VAx?91xD_b}P*{c_6g<6&3V zrE_8qrib6!SN(X?7mKx;4~+A|tJch|o7TEg>AbdHoyN5F*`2RWby<AfFtPB5o%3VX zm*sh$X6c^EHUG=IAMe|l-L<u7>ipIBcg2S+-AM2dk_+=$i@MkCe!tIJOfN=)mzUS} zS7_0K^nTlK9m#!`YDFiGUo!@+C40Q<=$iRI5~I7<b#?J>t?pWIa$V7%815fW#6LW^ z{5t7bxm>qu_O^R`Z&fF^A2mNeOD4IU|JI|umX3tF2^<11vKblY$5k=Q|96a*UJ)0S zw|n}}@0;gGi`G~%Tr*kMpZ!1IGv@#CRKxs8XTfihA)$i8?pKdZG2Hrb*_BDJTQZfc z+HMP@t+T>X&Xid)GR}{EH??BHJdup7Ox>t5(bg$dMT*5l=sl&P@E};t<1+Tld*FZi zd%FiuG+zI2w0}8uLV!?|(|cFfDT_>8yPA}Qb}4aaOq*}-DCnx$<rNyha#~)LWvMy0 zuWEw^(=X=<TB-s}Dgr`YRXhLJJl}NuUfTPj>1TAxcYZ&teRJ;3na^idp1=3*WzDBr zaPa`Id>te~^KdI(5w{G_k%`HU)$tWED~=jQO-4>jqoFYx8dwSwaAu@=+PL=ebJOkg zWy_XLsrp*=`RJl)vllJCqTMTZ(faMzZH)PR4<rfIehLl^4%@{AK4e}{wW+^zCTGbr z1;2^?6W?|=_^*Gnf7zS;U){fY@6Nro-!VN^f=~tK(7@2RiIt`1lvv!2|34q6JrS+> zR$IPNwCr6~PTnkw(z-ZF4gDvRd$)hgs=cfyEpF}CJcEar6Z8U<6&?gL2VHyApWof! zA1t-|>jCrEKXm47TD^4Lc_E$s2Dx>+9#6E)JaQo}^V%HNoAXyZnS9Cm+MD@hHyl*w zZ(6s!jzA|IR0;J834F+$@TIbT=EUAFX$PJkefgP7veI$dv!Ab-i-X?Z)6CsjH~-6n z;Ln%O+>E@wH>m2n)!)lyuU9?^=Gfu~j&FG3i<OZ-v7y1g`N8?$pTF$cbnZqjo7*|{ zy#~tMKaR|a+Pg``Z_1sMa??tF#@{rFUGEn^VcYE`^-&Z4et-Y}oVW3Fw|dRD8!jH$ za`l$hPm>o$(nOTh0XrHR{2wm)YW*ir>_g{)vvs%Af=g2S&oa)>xovY}b4h={t+Bap zO{U|mqP=yQPq=C?t&XgC@lWT8adKJkH2GIk%Wjq=RNa<Poc}BQSQO#7cepMFN=ILR zKAgX)!GGF^oB6jLD=$3%*&19D`pI_c;fP63m&ebTKCdP(?pDs)y3D^<e=oiHdG@CX z=WF}Vd2W7g^8Lp`-S#EEVXwm_k0}$AaLoll7PkI+IRD-W?eA~)$G&bjKXtoX>ijiJ z{HGnxoVRiHrzMY1GjF<cZo$F5m(>>kUaEWT<@{&o{3}nS-}<uiWqkj+vsyowT-jaj zRlPquBdR{+KHJ=liA2ODOP7Ga2TiAq%(Zi>vsr3pU1Ge=d+yZ_lcmp>r)$p1`FSe( z-idGLnCv1CsbBlPYS!k}i%<XEd~-V+|EtMnQH$emWi>Zu{%cxhQtqhw#906C=L>60 zs`IWDPm{I!V0(J0p*Gjgk_VG+wqQ+1D5cE>RTh>SsS?R^XPayUZ)EcQSfca5SpWRj zn99?uE1%9&w%?UBYx>igO*wx5EbO`epE>Phbu%aW=KN)U4CDGsKW}>aMv?dJWo_TN zm#4&475Y_GKAMx$wT|&+?CW&K`8B_Fo^0MQ>HHP*XK9M_Z)RSbGv!y>@0yzS#LTM? zYo$t_wGp$~AVP|fasJYauaE3|e&<h>*n7{H4c~dr{X3`iV~?Rd|DTx3V6hJ;|NTC6 zV<I=RT|Qg2(uK=H?^bgCKJsfS`xn32{Vyi7*X30uRQ>uMy!mq2Yu*#fUP#T558W3Z zsyd(J=by(1BQwLoPA<=%_SUj!`oqjt#hZI#rHDyB)4}QW=}$+o4=0OkUN(GpJD|R_ zGe1SNW>eYG-t|xVkINWnt-p6UX5;g-r);l=*34qIKO0(iEp}bGl;sA?1;tPAu3vt9 z6PKNJo}I<o{ZaoV)bh7Zt%+ONQnSg+?(grfe(v=NKeyabP@LZyBS%E4iVbo&u-tL_ z)8FQMr?J=Y-Kq01Qw<M0m|hijP`!H3qhp#se?8`pT&wzEv%<0mr-H1eI{34cDs3^g zU;W5fy*lyenyT|JR=u*e-(4Ct>&vS@TfT2|zI^KR_p|3$gq~QwYSMY__2+%d?G23A zKQUeqzy8a9d8vuNzQ4Vyz<L;ayA?J0m4rDQSiU*Qdq?f+2LEgBaR=4CAI5w<K7G>% zzhJAE!r3X83r)kHSAI6+{*`juZ9}@l{J6}D|5M^-+?*2n?&Z`r9iFFWPMg{Ny`}lH zto^-}&DWFLR!{kmDf;Q8lT-2Y<tEm*>%7E1{rtT@ec8=O)%l9ezn1*}`QXD9{co2E zhFxQxzCrS_o|5CT<x5UZR$m^uIc;i{9Wy(hkFtB8kM)}k&QdlzD#I2BvA<b3m-~mw zi$n8klS4jada=JVFsYvtvGT#^_LCc}YM*~F3}u}EjLUB70{@UNCpTGEUQp*NZ)Mzj zX@Sm!%jUCVU(cNWY+3x@)F`P7`^#@7eEpaCY|b*1<ZDeP)p^0+Y8+=x)z$APa@ec% z<MNbW>9={M?^u65^{w{h)SOuxa%y$zGtY}ksU%7eEKL{aG~V2tzFd3#9wXNi%O~m{ zvi<#TcYfD_{;)j_4gMdOl~&vQ?qC0OY8BrpW;;#W?`~r2PF{=o_uI4b!?P{Q*G{ee z;BsSm=mz!Zz8b%{lj_xm^TOlg=bSxOdS2Q1a&3RA{gl84_3GTjzU7xstIKaFy8Cz= z5l8LWF+D!k>)mhv??-gef!FKz*WIdoKKK5ljnlt6vernIw4VQUc=gXSk?iS=^Jh%T zekA>QbCXr%f#>D=Def1hoU_WBJFUULrBdm|=d>B>s~>DG*lG6S^Y7hpzK7esc`8qP zJ)4d1)m%277jr{jzMNW>9$azuwAsA*r6RxM%I_Bc+)@30S5{T(tv@E&znZ?h@h;w! zWq-5#ruK3fi%pI~L=;7*gm(OR)a^Zg+Um8*{jY52|2U$4tLo;jr2+yUBDwaQdozo@ zW|Pj@gVskyVl{nNgnZbUm?ZZsGWqw!%$@s+H#^zv{iJa24QuVCCzgLVDbAX%{d)SB z!e4hICum52KhVEMkU#Uwg?&FQrr-HiHEXi()30Z1!{3~Fd4uo)v@e>N?0?@pFJ`%F zhF$jWx7+;}yY)uB(%h1EcGgri-&s>Y!+N_eH^`P=3GD94KYEFq<A=`P#P%wS&)*|i z=dYP{PSIL-I(zoK)gd2t{@a=Jf7zG0waZgftCb#1o_e?QlkLrz=#K{WUp?0^d-$@G zHG7$)ueY85U0w5@^ZlO>>^=VExMAAPx}SxAo_%}y^KoG{-}?)dH#$)BK9=&~#V)3; zjPn_p*_LcfKE90Or^wB;GczWx+w<v^N%glkVRyNHY<Z-S<9J}X!KrW7R*%asf8y0* zoX;6~KwZdYdhGX{S9AA%y1RG&x%m9mPX4d%+V8c>xNNfeeb>#ry^A*Z$4y+mxc_;~ z9~ZeNmM<1(KV_VMG4g=A)s8RU%s<uj-CjMT;e5{;sSDouXU?8qdN00q(}PP|r@wpG zRv+ig*ve$6d)xN$eGmUVCfc@NKfN)rkKTCBi}}(nZGNBI>ugQFJ`D3tn@PZjt5~v_ z?X2JJNWN8Yn0NIyjdw2}_uJ1q)+>E|l{4e~Fqu|ejvpd!H|yM&yB|>3`tim_r@p7b zKlt1I)a!lzdp8Fh|NgCVSswdZCi}fr%UNsNQ*N8KC(lxnx{{yq>S}$(tGnepuc-gN z9QOMYYyYtiFBjBaJ9oU7%WmVNNj48IPuchDp<wRYTPwes+nVUgR%RAQtXwPm`|Hc8 z*W@h?_Lkp$ddYD2nptgM`a33<<%_+seED7Ya`XA~E4nw{%xb<=Zf|6@H@RdFy9uF! zX2S}H+yl=aH1qp8w@oRYU-|3H%a@nD^>a%FnC#YCa77Dz(Cpdtx6g0CqSg-`o*yaw z!g28uA9g-_X;Toac`}&u%apA(7wlfAJzi>(wsYUlk1<8jVF%TtFP~`eH%su2PImcU z^6rM|<=xk%Zf#npZnyK_{ou>b_2usuy~<u^ps;r9qXv`WcTHdJpWJ-uisa`{*B3RM zf9k$w{<f2=erBLFbg)b?EC^d*_8_?I!Lo~wug=T&j}-fmd10#E+g~exddSz`-%!)F zW>1s9&&ATSlPgo_K3lnMR?zw!uGK5H1Yi5)_<P;_{{7E3)ct&{x_$3F`RgC$HTN~0 z=d4~cZ(H>8QUTQ_E&udj|JdsPSH7(0y!?4%JO8W6dA~2IWiDMPX!WX6ciH1xpO5!1 zeR=asT6X%vdB)!)FKZnzF0Tu=`v1-I<r@3F6CSVGR`dVMa{at1)1+@Kf1YSb;P^d( zjPL~)8)v)qUw?N}|FAyS50e*<+zzNWS<kPif8F3;ykNP|o6I}Or3cjeUT0T}%YXR! z^I-P!yXLZS;TFei^tsp0DLno8=j-ZRJEghx528x9$6gDsdpaxqa_V<q&*sf5Pv!mj z<CZyhD%%y?r|aYG761N{*MGh~w&207%=2eo-@bPDrWb+4@<NcQm+y{A)gHM*!Pj$^ zH2BL-F$$}Fzbjmft!CQ!UHM+;_u1*s*II3)8gBME$a(tpGs|Ciy}VvuX}kW(=db<i z0#0P!|8ikV$&;o2&uh1ef9T{@db0Vvzr6o<+uJcGpHG~;&BK{}OYBs&ZT(qhe}A`K zo3yo(dsfi>8PBscqh;Lp+j-yjmpQgA-Db+X*=Fi*cb)w7@{sej%+Dv}=2$4#8m#^z z{Pu;c+tT&#_eIUVUDZe+e^)JYSj=qRE4My9?rM(Gie+oQUYVT#q_Y0|t?*9|?KXOg zeK>hyer;`?*o%`|-%|eX73~i^sNVN@Ezb{@6aAaD?3UlX6La+WuJbt)?pdaJtUmqe z%aofhpV!Zw^Y7P@*O`x27hjrF>6;l+_Qr7P)rGg$?75+3Civp#&AB)K`R|*jq+qOl zyZ-dW=F8h&>rDT4x2Sc?u@C=VPO6SOr~P7$#ghD8FD{fu_r~unc)fW2ve*A@O>MvY zIrHVp>CKlq4%S{)Tg`WB`Q@$yXD6}LJ^GRN=lqtM8N6=1UebM15w<C-pPx^EzW?h> z{->|)Hvc^#61*UjU{;EPq`mOkb+#{eHuyKMD`Tlq@-BU$XSg;fxJr%5?q-qCMN!Au zZ#<o+f7aZ!>G6-R^W#sq+b7OH7XE?rmwnp9Y5xD8z5Js!=kG?t%>Cu-X4v=dPqJE- zf5$*ERdR;e?QgGjr|bQDva59Ur#*j;?J4<^Ki{w1{;%8Jzu_mFoWkXH*6n)rKhE>{ zzo$n<PJfr%e^cqU+lm*r&Ha4#|N8CSy(cVk<Ik+-%l(@*k117zgzf(MF#NLMbepN= z_2+JwUOs*=@QkVU>x!?0ZUXBS&nN*E7^ZPsvU@ZhTwd@xK=wmtmu)5M{8q;K9KZII z_ioePE>hT0?mOqMkV4nTYo`wB6w2h69#D6(iQix6RM!<~`@B|~@AUlplUgI~wi=uf zIyk#_lfmg}i`$n>ym=+*{5~V~zgDYveeQ2PI9LD89xtnR%&WI0bp>2lXJ4aYYnB<d zuWs{IwxzZA_Bq|MSiQqomHXe3EeU};)0eKvI)8g}U2@jOZA^B84qXaCTgum3s>uFW zH}w*mGW#;|_}Q};tvdhoyYI>P=*pdSFV`Q>o5c40{ak(VpVxaVxi7JwuQsv$`RR>G zmCJvVciyvgu7;$#%FW(Kp!vMuHHV2+!Q~epclDJd_q!Ze?s)J1&Sg8B&a2xS*zjMo z`Fv~78$<0olmCi~)}G?~@#pK#$hF6!izDt!n8nTs{m?07r(W~s>?uocXCcR#%KEid z?;ky9Uq83(zKNFD*1LT>U(GCex_{Pk{oT)3&t4ij{rob%)3+4&*BIUXcmMjO2|7B( ze=kbw8I^{zRPgQiu>a+I|Gg92_+HGFv-_AE$g*;&>->x5TlQ{~m{_;r{S1=@`L7?G z-TV0d2`3l-<tvS!J-vOE`-e+M#{K#K&FlYe$PM>Xf4|f1RjjGso`S>7!Iz)wNzczO zextW~+5g)6T0cK4{i}L-b?3|ZkrFSzM9=eoy!qv+ZKswQSQpPX%-pM!u!PXQjK-)& zH}*U!+qkzszfDPHoX-~f(v{inrbz6~dPCQ6yU=j4FPSGk9V)yQ7jf|UBm2FjjPqA4 z`n++z-8}z;Ti<yxU)qsumbp{vyzlJEVkw&Q4*vbUWNKSl;u0%et>-mAXQh7*|MzXG z@8?ry8z(%SdZ{ekcFMfjR_eO;EBtr;-r>7hAYi_g?UFC2b8f8_uKynMFKTbY`MBry z)85tRxkcqaZG8DmKj%u?)j5kd1zhfp+@Z<j`MoxGv)S?0)dHs4_WxF^U*2(i^Xs6i zzrMen>i6q`)XNEL!=qFZLblFaPN0mqaF|s}c1Pxy3fr%rK$X*}`BkN#P5j!^L~E29 zjsAUIljS4klBs7c?7!*Sfu~!=!anSL;lKWwbNymR(Hf;1<CkA|R$iU=`PkkWNv~F} z=qSD#DE;Je{8RSJ2lv0LdGxj9g8sY{+xI={_<DKUYo4?7{ZB1Bck9)b|4+`_?|!`E z%htPL*_)5={5<<pZv0HU{E3>o?#-~Z&#gJd{BvLY-O{aJe$I8<dh@*eCe1x|lYO6x zzAc}B*I;jL@4?)>-8;lzvDO^Qn7y@qnXS3}M82x)T`T=_B^MqoeN{?MmG_i!zRsPZ z7we`r_=g=-_ud_%_2Z4ft~;93-__)mt^RneZ{>zfA3rzvm&M$_vHgeUE;pWcv)R67 z+s?nbMe_6IGdsDXI+CN-u1~ofRh#(y+w!fm=7)anOt_G3{CxTKG}Hf`D_5VBHrIT= z+Uh~EzuDJo$Lp@yti4wGd;L_qPum1_A78CDGMnA>`dyCJx?5a!{C4)rwIB8z{}k)@ z_k&dC>5I;<-p*U!{c^M2?t)qC`g(4rx!>gG-*2!v_11>)O)n46n`xvTTl`=9<^IX( zPj~0eWvxy6wfVzU0!2iF!&Yc*`75USQZXpmzs-*q6|I?dNquvJf5=A3|50HF)uW!o zZ+g0k*{<9tgRed0ShV%(2a|bc>+Sn`_02}j+T#0DA7xD2b$5zyaA}HJY^Zyb&*{=< z$FJpVE;|2t{W(wZJBF5jmAUWR{QR`WBt1^1@Nbyxhn<h+2VdG4Ztj1#^lrMD$9tK} zWd`b1-MPv0Z#A5MFE`_s(5;pBS%0_2pJVcluZjQNxb#=`w!LPj&6gJc|CIFW-~9L! z=2E+#a=rZhd)>`%d~pOutQzGsFT6P5yZHF)iQ=Z#nIMn;G-tL`{^Vu#IR3QOkCgwR zT0g#=nq=M^6Q{oVfv{-HEM_}R?jIuY2i2W4r2C|<c<m^EKjm6fqQk9>sg9Yw5C6_i z*T~-L`q{~+@~eG#Prm3Y$xHjor3{zkOWCdPmonSn|L<09r0D#NtBmudHzk~VE*5rq zr_JnL*}GeV%T2%Ud2xUK{+gOy|LzBibN-0=xjy`oVYuz&_Xbri|JO(bmVJ=T_`SdQ z=$Dso-OCbApUW2to^<p|O5o2I`SCYR%-_`Le?y+1!O|vbObvz9{oB{XZ9h79dc*lg zEnHQ9Z-?)j!&(s(cTjy-&90z>%NP8!zTzw65W;gRD&#|FSzySAlTCK&eY*Ra{C$+S z{$6%-O3K_E;pJyNchC9lQI-6y$>vP)opr01#+t|3^-4|i)egI=^g^g{I>?*sYUe!U ze-(bd5|#eeSncip(BBW2?mbtpbNBz99#<y2?`grcZ#NgLTHnnVYrZ%CaAN4<{igo< ze{Orfd~)A@I{&}bZ)V)P&)WBL?L-4D0t0<lgdz3)RqN;THuxP_?il{_Y{U7EoFmI@ z9ta1QJ{7Im^mMKpzs^^Wyz|Rmtdh&0>viDyv}c;Tb_E?+zN_e5Yx|nXWp`74?lKE3 zRbTbqWi{WAJyykax8B_5fAac!?QyP`?8&$1oqF}u{MpjC=V~<0&e@$XvB7`Ox4!pw zGrkx9$y>ej{96mH`nV_Y{d+8o*Pjrsb$dGh>E>51Z?|T@pFb<ytSh%}$Ni|6i}U-x zPbRpGY!<u>wgQb}ovMG`^^D7|{jHhJ`X`^ito}U9y|2qE@<RG@!>4RD$84=vU#)np zbKji*PfW?{Ej53HzrVH;t?}Fa^XHexx7D8>|NUOr%wId@a?#anP}}C$SNphg@vHcL z9O>#$om&3xq}GQoZ|`c~tvhop)5lCVV(+)L_C7xL`?HJwUDAqdT~OVBf6djj_f~K1 zUmm^wY+3JS&7*mL{<N+1zZSpOEWa*z)-=}cMMu9R{{4OJ{<^aJ^}aLa*%cjp{PWfO z_q)@&GSA-14&1B0>W7Q;^tsn}X;1yVFY|rG9ztOd>kmn;*S{W-E_PwE>n-%lXRDc| z{ljLe@5RuSD=z<0Iek-S&5O$&PfMr0pZ>IK)+JZ-bjJB>N|!dAU;2H|oWA{ePIWQ4 zzP+Lk=l=bFUpVt#6OV58R>t|&^JmWc_A|4%AZ_nd_GR4m`~0HPmzq1<CD#6voqTEL z_I(qc>AzVVci{P^-^-S)kNKQqcg*}-gl%or?}<M@zuNyS?cuz`dUaahW=+Gsy_fGE z&z%(}6=gpqWN*Ap|Ks~_qWPD+EEapBxa76{X2;pT?Y<m3v*o+Lod4!cS<V;#@87qr zGK_K6HuuwnYU0VPS<L3_@-+ogMkyYeDwmi0`%jOn%3Qs#-|E$hu+w5)f&w2jj}>m* zJ7-epht3D*5_k8X_-rlsp;9|K<U{8v$NjrhwSJf^o%}6+=Y+#MUoOmidBS`Cx!=E_ z9m;zz>*iNvhW$FQv;Wz{=^UGC%w|hvPw$=PpPd$3q2@DpZW7n}`F9L0drxV-v-y4M z&7S|a8vNJ%c_ey%N%p&X)%seG|MxOqZ*5=Z8!kH~?$ok-U8gMj{bx=6b}PxHYmtrN z-SE8){weikzb`I+c`-+8Z3cVJ)+DQ(+fy&IDYHNO`zQX|`TTctC$(2U`l@`-*l6yT zg;yVEw%<NlzJHG2j)j&NwtVYiz7%e^A?sH1=7*JE7rszovfY2S$?f@{r$@h>`chdb zy0Du+*8J^;e?_u#hua8+rZMtBkGJK|<8swHck6z?UHePyL*~)I8%+)ViBsN|mz;WP zI5mW&ChUavb5K&7n(7|3l;QL_RsV~n(PpmUUn^oQ9|%vf|NZ*Om$&wQJ<E01=FgAQ zTz05y+h?}Jmp<-ZJFPuGX=mI4^;KW~WZda~`Nn?l1TnFfoxahx-0ztw&9yE&yT{4D zrR^1KP10iyLD|2zKfY}2G>-b#yK<M-)Os!L#ruml{Y#44aBf@voewd)Uc7s@a(mMK z7s+2A{{1}R^5^yEPC6%Uv^!?|v3d3K!`rphCWzK%5%BzmHyl^kYS?%rCLHUNoz2EC z7xTi??d!bN>vn0Kop(28#^Osm8~j}t-Ee1|pJaRc&D$rZH-B&wv#`wR&*@tC=Htl~ z4}>G9o@h$xe;-|(F#EUI$3yX(8vMUjyykm9yWLH1S7p|}icgG3S=_o-JP5YDFTFX- z;r{NIKfipLVs6iKEG4w2>}Ezp*UL?6K659R{rPwJ=d-U&c2n!*&Mn)v>-S6ZOZD^S zPmG(tS7C4US;frRE1qq>^<Yocc~Auy&YGqDK=`lAh8JseG)h1%{o>2_c<vm@j$UJD zF@IO~`LnZEGalWgnrV3S`pfNmCdjSf5(@qPc^_~4QsJ_A+gyWHcfNaloiUHV;vR?Z zLMwQG9J$CZWuoD#I)9?F`?5yn>F?%!UK6?b(vy>uKcBKSPB%N>7OboCAb4%yonprM zk92OoeR;OwJXhp}Wh=EFOjfN~bmh#U1^%zD&Uni4!{x#AaC86DZTViVtTne>BMynj zpIdfCYJTWN$+@9hIx}~6<+aEE3EB1aV)*4MZGNBawV98Pmi_;==I6_eCpTZZkaKIj zsoNKSXZA0VZ|r36D%|7p_nkQv)T-+}-{f0cwR}rnS<LHoCI)MF)qH$@@8aW4vIMHg z68=VSy`3s&XMB9dW7=!;YK2hA3B`7kq7!n)(;EDn^}e2*zIO_HCf|MKWiL)?{YVKe z**i6N@6*j(b~hRSyjr%V$^To0m!oLSy;H@j<bF>SPhYmu_}O#Oszp)$(YniS-ul(k zerd_|XUk?Jz25WpdRBJuYeJLp4%5XY%=2QZ{(L-sf70VS3C+Bh{H))8nbxy+4Zqfd z%L}yc2z~gOAok&;kSVX;jmys}Di5ez&kN?$N$Xn2R;X7tL%w(Rlyf_t{;b(OUw%*7 zt=h{EOTYg-*DHM4|J|H9`@hZQ@AsR3BW>xf+mZM;Z!<En>}q!KnO*wzYPk8HF8`4C zlGbH8QU(bQ<q5CKj)TSzyvr`-t3RJww5ZAdNWHcn>@0xrto?fmv&t-dBM+pfgj=my zYBx=MbC-?0#rKIOxzGMw*<QYW%XP8CmqDWj+tcflyf%HjDH5^&-*NAk_tKX=Pj;O; z|FqrKI5X8Sm-Pe(aWB-eCUvc|e!s_f?~g~_ncv>rTsqH#r7A2kb?TPC-)=AG_?hvP zgX71P$Gbe%tN-0r*|jciZ&cRzCytZXZQ8SjwdN6%&6h)GN<Qe%^9WwH-Cp+A?(0&s z>WUsjWj=lSOSkCm8Ujs7Py*X?m5IrYcNdfTzh#wDE7*9izf9a$dSO%O!R1T_wda~j zp4u;8lKpP}EKvQ?Ui<6o%c;kDBqm&oD|Gl(VWxWhoHWnsoOctkv`;~XBTYYCc*??3 zv+Is(p+hRynYs6;_}`o=9;8*QP)i^qgG}ZEonyxlWKN(^gbX#G0?qI{ht4NT@u<nj z(KH$wqoD!GM3}=0qiJI_ZH%T3%zS|<GMZCHbINE=!ORz!BBMEFG^dQ_6wG{qDKeT< zMsvz&PQlC<m?EP&Wi+RZ<`m3)fhjU*a!SOjhV#qq|GqTp^CoW8Zr79p{`J2uZ+}w3 zQgctJteL<)a-bnZjuLZ5#`*8cj>_mG=MT(K0gE{F3kiI9894p+$x)+60|Z-fGMYlr z#@9y!WQYgIQqDJfKA(GO`@YgYs`l&E;P?0TdP}<>2l?tkEsK~=!~`|JIWykvdcCgX zcJB7Qs_c3XK2O^qJq294B2Dxl=a$BGY6UkE+n45UySZ$&?6k+AMO=+~Dh1!~mM@S0 z_h~w>y%OX6w#m{H2pA$TwHkDY&Yum>&d;B}D5@E<)<lab@akIs`F1}arB08%_QA`J z$!@ClL%efQjdI!_dh=_Pd*y7cIKO|nvAsc9-J#(?604Ttd`@AtCBgo-r5kUH?`&-F zpE&E660%>=tBnQGTrmX)SvB+SoSSRCJz8#mGw3j-#$y~`USHQ=|EcWDMR)VG1FefW zewa+Tod60(oTinyHlFGYW}LsSqErTS6V0wZ2bP*ewZAUU&wJq*x^KCZRfbu_`ekc$ z&o1a~i<*_Z6@MKT8@-_Damaz?+41V`?hOo$+oK!~ET0?>A`;B-9`v%nmyvP4S>_(F zOGBBM>_R1EE43b67F4~BBZeWNazT)haekl1P4Mn!mKv*{asnS-s?0cxcfvR$*x|r( zUnzZ%$qrK+8vI4w+~;xqC~-MyhCj4i91bj3_SyzYOpJ{4{r0i5)L3z*72{nJdP_v$ zL+0dT;3JF!R23csw_cpMoXJi$a1$@Y3t%r}+AAw4@Zn|0q+C!f2nxXWARC)*&cT~t z%~)A#ZY}x91Tr~<ljBE7V#$-04gT&^a`0}NcW`H6sj*s=A`SA1z=xMlszEl=Ey`Z_ zG0u<MTeVbB*=@<x@Hk6eX)_()nMSI<vrII7=iBX_oWA$T?EJl7uVw!K`#t|<!mBGQ zU%uIV-p@GgOh)8$&c=0m4{GmRT|eP5uDdetf|B6xIBR#m28KpAE{-21LAzJ8*SPh` z%=~sExqr(!tJh1WhDB+9eS5onqH#}yKTGYupXckB6+S+;wDR*a-A%rN+F>z;M@t@c zsxNC~W`B0~>|AT}xs}gmzMPppPjgXqtw2Uc!}mhx8N8bU5)L#lX5O#;?)!S(?!0bK z7Nzr6uh&dV^#?^H=+pqzs7|;kB=8|}+q&hO6&)HJBAJ-%=1zILigEsmE1RFst6uhY z+iktC@9yqCoGuz#yKeWpT_x}LeqXk1*)qQO+jsCTuDY=CxZLb(@%4YF`RecaaH!<G z?e{G@)lLgmb63chf7RZSBYS^_Ve%zGcbSQ9y;4_S9Z{N6dB*VgmZ)?byIrQqGBVCj zeI8{c%*4VmMN{FyWzW0oBtG2CTt4^Gjg85#!(P99@zVD1m&-5b6rc0loObrr3%$2_ z+i&OXURwL@=JM3pv-5Uo##X=Gs(X5#zzaU6d&loS)GGa-+@o+)Z}*!?-|khv-!=a+ zlil6_Qr66%<c6_QV2irKgUj>!u7kpBfs(?5;E69DB!_eV09O(X%zP`VzdSzPe}4I+ zE-hQbquxHITRxrAUcN%eFz3bw*Kd^%8rd)Xd_I5wm8msu{c>~r?En3kCVkcQ>+Tbb z(T)pT7~ek@DlEOLBryMIyKGs+t(3{WQP-~834T~9x)JN;$&IPr4hPb2?wWTFRK}!o zf)dX*ZPuD=5y`z@zOJvg{rd9q@~1jUv)0aFdp#{W&-1+P_n6baUa8mq{^ol-e}C<* z+V6KSe|UI!b-Y*opU3i-zg~~OpB0-f_K3Ii5U2W*(C}E(`)^yt<2;tnEsIK=+Gtg* zvrTow5~h2{y|vQ!pX12c_w(7y3(ow$r**f-tiHR<Z|<z5TSvWwKdkhqz<Pz?flZnU z4>sq-g{QkN&}n4l7Fz;33(oe-1!rC7J+EWmd)@zaZF|%-+elDpVO1HsJLtf3@AzLq zw=x#@M$J9Abb8#ZcRL>Ug}uHNC1F!h5SN*LWksOwY!`<YpiKAw@B8}bYo*Ul*8P4v z{oen7-=FVxxw_ze?fdG?Pft$HeR*gb%e^AT_m8*#5z@KLE)c&*{`ZaJK_LmN^FIER zzASmK`u*OQM}+-zDm@P@Z#@J`WuRghwN%rY%*14O`bp~BP3m87Wv^d4qjBl3-0gSE zR+hiNcXjo&&gYZWe6vdLTX%)5+5K+U=_5P$e!1lRa)L9Tr~dvwMW=txsrhs=bN$|L zS&`pTZVH16G}E_QjPqCQ5uRFD_4U=&A~y4T70J8a?Rp(G&H9tdvNykOUEepYjYsm* zrpl22JAb`e9X-wZvr6CUBA?Ca=g%!?dwu5LHq{HFEPMKQ9}0~y>sMU$)y1Ot>(%i5 z(x#86uj|)+?Edm(vj4Ksut?RpMW-}%*K_~a5>VlUnco{28dHN7oM|{Ooxf+|GT+(P zGPnQuzj=Cm-A}*w>Fw+EKfJI1Z!KH*<KbGqFYoLBSHHTyzkYRTZ(PT3#X=cjb-y$J zkL-PRH7r_p|ME}Yw(qw+`o?T-?(+F{Rl6Q_X-Ad)erCRZCMc6ld-_WK<K|B%l>48A z7S5~t^>WL%TiMHf&2FmvH!gW`;gzrTmzsNfDu33_U$N=)hr|5OH-GApv$c9(K09|? zrm44oyVaV_fnREm2)Zw^EPnQ6y<21dX}#S!6MqMkmc9M5N<6;C@Tg^IRL|B!T-qUG zw`+S2DI`kQFS_7yr<=E1BW=^owAovCzn`>E;{S$!Gt=jtT%W%5Qeez^iTtys*K<yq zlq~zt5$YQMG5ypO&7U9hcfDMe+01L^G4+9O`mg9ox!R9SthNXJoEJ4IIc`>MRjT-= z%uK5claT1+QmWe?wQt?!dbHg`v22^GYG-Wu-L0wHm(Q!xI@&%L+&nwVbwr=XaBEWO z?$_&fe~Q;jKRavc`o2|JS5-wGi=RsFx7}7Heja-RAwg1T#T|>e*5!E*(@RgPPG2Jb z=K=fNcjj8nN9-f6PP1)XC-$N9x5ECPXY()R*Z;mPa<%o<))jhNK*gVOzl{-p`jy+u z?(O2Q|8cl7V#TK@kuwsOMJcI<^*t}wowNV{=gaE&zgkzjg<J}|o%wA|YTp&le_b$j z@r;cRuLS%18mFJjF%3&qJf!O`TN(l?H>~fZr{2GH-2Tr){<Uds6}NBSoA&Si|KIiA zck@%{aKz0mJSO?_`~Ls6siFUVH1gLd=zCnsN<HgzV_x;Uoj<?!KL2_vEIN0p-S3;{ zmsz}96!oY1*N*LGiHBNt#kxhj`F;O?UHfXwz5jl_{`q(B{=aYYOMgq7WCV1*^4$Gq zllR^0-7%UEE_<F`C-k9Hen##0yW5|>nEs5dK;_h`e7W=U?d@wncE|gq{t>!ZbYPx< zg2MuKt`PQ`N7EaB$cKHuUth1y3EK561}euf^1+5P+#El2K2P7_JvSko>&Fq>cmB1n zf?sy3&wDZ9;(<oy%jfI=ecrR`+@rFNSGvF7Z1z8$20A?;?(VWw*KaCXr{@$L;(Xa> z{cc5E?DwDFZs*V6_xtYqWh-JXhplFbdi2lVJxb)!iofj-WpA!8o6T`Crsm^O-PQee zzh1a@wQkvSqf`C=uj~6Sd8*HyGOzyMPuFizF$bPs4UhLdE?-}>>-oHDv76h=Tw7Ks z35Yf>Uz%H}`*_x|t-0%umi8Ey-zhx)&Fu7Q2G<*g_x#^(wP@#9;99WS=y726ZT*+o zp!)l@7UO)6`d(S<ZBJi3=H0bK^OjxhuO%AHVSn9z@BjaMzewhuzAcQ*Y)|AD1v+V; z%5*)sPlH*E^Yrt1)q3*}8+w)L`gYeTcZdXcxvnZYDjI&KKmXH{lbT+MM=b&?S19ZD zeV_ZjCjIoX;N^a2PdA2!xiFcW+H<u1PlurLr_Y@)iyt*>|Fik`<FTgKA>GVw`CYu` zcMQx|f02KA)?n42&Q1BbOC_w0-fc)c{Ao(^l)mmyogW!RuN$xPR1uS}`Ebxoc!TWA z{cCftMd$AgTY3HepXc^#k9A(E3#`pLGs7@*>C`Z<kE$}qMfMq9Pp_%G?tSc<wEjJI z!?>kK+q0&;=@wfDIy}SfYT%yoL*|oKy{(-v{jXhL_J*I&W<P)butNTLwFQ&hN4Y-x zz&<^>>06HeHhad?Yq;or1-}dT{U4H@+4p9C53SyJF<&L{{@-`y&z}k`{u38-yW;<` z^Bytn7W4b!PQO^ct@WZ<bk4;6;g5_}fAxP}@u$b;_nXa{b!F4Hh`zmg@$m|lnnzVf z^rH%Evt~E=ukA?1+S1Uu4XS3oeSMpiem#`Qu2A1()uStH4q>sSSO3U`-0AMB?)|uY z*U{RQ92buKo$@UzgvILhn$1yWKcA@E&)oN+Rey`l@yQ%1E*(p^9j#rpLqGKPwmU_q zZ4yFu@7pAkIOCR=`Nya`+q-AVXa#46ZtFj;bjTu6^80t~wfXn=d4~$Bb2YkzE-uJ> zJ+HKyalXa*zu)iMAN{-Mme_UucOS3bbMHz1cd@^2O8oCzwN|@i-Y>22zbaRJXL+xV zOYT3#Yey@O*`BY`b@DP?8vXX~o)h6mY=4BuRi;k7`^fg|%=0yA6TiBy75MOSNlG~9 zkDNUpkG-t_|Ghq1_U|OWi&s}o_S11uPzoyfcsROJdF$5dJ8$-XI;AZ>J7@LX`V$ir ze@a-t-}5=?c*UEI$1hzCkKcR4Q0b4r%Tr$6mY?c=9F~994XW^ir(|X4`(2#jukW?R z^}=#v-5#YGqG^ITnd@B>KxYEHEy!4TDZEC(?UFH*&(+4Yr(RDz{^LN8rOx`@?{<mE z`?zlXk#wi*c5d{pZvC@6z6mb-aiQIA%DwOVzHgnK)vJGMhWMs$-Mf|^UZs9JcYACc z-}+6jZe_3c4Sc7!Z1(RH?sk(Nc`w-I>a-<JVac2Cg<s^K%N`evm&@JyW6{so@&9Ml zecydw_mk?yYjzv$)_uMfoj=#^*Tw#4jJv;W>;0bhr{~Y-`TukBH~-nXt>&=lrE?R5 z^78er{rK52f3n18V_hbLz_~h1wQsksFey8&yM2j<^^KWX$*GlF%hz08e^h^|pwI5l z=d4$UZ?DpOaJlO+6R69D(UO@~+33br^Jw1LhwbwFA}-I+Q-3F#aeAIZIOmTewnv!x zEEb3s+$sNeZTr5fujH<ThR06b_kHjCt+({vO$N0U^S7=md-!?&|C&1cEt~$O+y9PS ztt9aMUiEt=$DSSGf3~i#)zw!r$h@?q<k!pP%WvoJ-+RgR-;c@j|ETC^T*yk@Cg3Wj zbR|eOrn9;taEbCeURTCWiO+8~pU*Qb4Y`y5`||vMQy%TB2+t_Y*LN~1ti6@}mbFGn zyx>;m@~yXQm;JT=aDch^Hpj6edpF%!X7xp+mFLmFq}<(KuSH)L4Ud_~p7m(nXLY-e zov!a#kK4J+6uPWl5PYNV_wD<B)^E22-%IV06rH^4c+GL^dlSXGPMELw`MQZ)FXG8o z3FUj$&h31@6a6jT?RfktNo8v6W2<9-PirIuxUGI-BsDjBWsU#TLch0PZ)*Q;%*x7d zKDzf>#)@ry>Opns^D5I;Pgh<cv~t^0o1<a1+qMdrFG!YJ&vCkB%hAHvx}Q&9Zo8fL z`hwo<+-;HnoF!hK>DwmpSik<K_dn))*X+JB**z@#aFG4^!q#oyR>#RdcG>Y@R`$A; zad)46UJ>@${{LtDZr@2^<pRkr50vc~tb+_S>MnNiPx<B~e)aG1&nq&D?^Qm38gH?6 zz3$1Py$${$C7nSjfe%+M*1f0vF^;o&Thv|#uaG01{9mfp#vNGx=6m6u>yMmmS8<eX zIl8qk-oQ$zsOv+3ii7#hl*xT%^Yg`DUj0$jGr=c%?Y#G~<#(r^Z`^s#>h+eb(W}2s zbm`MFmE{yU+V`<_&-J+Kt<rwkrViT#uFhAo{c}RO|H{5CCyHjqb@OZ!(FQHtE;*&S ze9ISs%ty(6mdjFaVsBXsL~LWxGWWCo%pYZ~CGcPV7k}M{=4odiKl^aE{QlidcP^@5 zUgn!^8timu&$>GY`s$DTd9k?v*=A0cr;}$CD*SWI7u5Z?u<Fbs!ObE^Y8PKz?Ebt~ ze_fUL*BcL|rn)4lb^bX1AbaiBu)3bYHE*}>I39ZUQC<Ar-vxnpm6y$0wPlBVU@gDt zyW_3#OGEd|C3dyT*IE2$|5=z<xPSe&qkAV<{I{4Vp`Mo5@wvY`|7+j!vj4Z9s&f8t zQBYdvEOG4b{2j+WE;m~2@>W7Navg7@%MYD*f^G$iwp|Uo)p@{;CG}QT;eOq1D_Ja+ zN(8@8Sk29Hr1t;sce~dw?VfS>>Gb`7Qti53Q&Z<;vpiBd5twNh?b%Y3bvu0jud6mI zf+OW`yH4!9l`S&0>`~3C_sz#@7utTmvw2hYa=FJbkLB;}|8R)=XO3dviKMMp!%p9S ze)av~zOty_|2ml13ilho>ptG2Gxb{70rk3jizJ*`mT2knD4T@b*4y*p(4Hp0ufJ9_ zi2P63cS(t7%W=8tC6%9_g@!FPK4+o4_y6Db`H_9<ft{K$tD{?vKKhqj+TA2**cx8i zJu^Y+MeyB6|6<-qf1j_ddrrG+UC8zsscMJ3uNt2e37opJs^`^nCcCRrvK9A0)z=F3 z`87qW&CA!UW^sCSc-a~;`+&mDJMXcV)ifLtu`+ZG+I#)+45b}{@(bQ>{c)j_|6tYB zyhrVt`&btLKc&yNwbr#lQR~W;?5|9A9~Z{hvM0Y0e=Zb%Vq<O1!YkPy7s?!5^)0IA zVBOEt@p+dQ2R&3-_5RB0UBaH{egtH$&)s@8?B|~;U;B*hN|Jgwc&~i!(ckvt=(@$h zbGDYtY-ODPBdM2v->m}wnhnR>3;#R{c|Rk$2Q<2(G531dYTgAdPiOyFl=s>;Q2K-B z?^>DtbB<2GE0w+G;wzCy(>HJmt3BE8a7<sLFgEGbrdML|41uYu;$H4QRd%s{$>i6q zxk@{P;~#zGc9yta9e-4x|7+dP6<^|CON&l=BtDztUg-9U`lI4mEi+#A3eR`F|L2*x zc=Xha{eQnjKk43K<hLjOkgi?$tG(@yzj_3;XGgEiS-<UA?4++ucHWE9<gvCJO5z*e z34B;Nw`zqEukRi0?rM&$!8eZB&fd`}_kI7gB}e~mU$N(6nCQe6TZF!QUW==KYqq=p zx9;;f#eQ~+UH8_1k#U^4LtgTD)a^(2+(Y75=6!Wy`1q^8{?Fqt-SK}GrC#2VAHH+h zMx%F$M{DnTb!X0dBwy{$*aUL9uCeG5+xZ-OLvH=}`1o^mi~0SU&q~33N6iwYzlSTu zT$tI!xs|c+h4#DE*VqD+YVWXjX=w-kSWtFrafL0D=f`^ylN`b;+*+r2b^kt`7`on- zb^eMc!I^JEV>@#n$**<)*y`1NGilZG*Io4)^)D8-hsB+fe_R*lb3EjB-j0WDVgC)R z<NE6#?0dOv_O_g;^;PHB*Z+NO_J3j>OVO(L&yW9o0ct8N-QQ=^`TMD%vd@`&f%(U# z?O>N)xbR0L@9n;AufDtQ30JNW`Cb1ic)k~?kKk=?SnZjcy0l{BwnsrMeM%w0@BfP0 zPv83emU2V*T;q-`i>(W1X-EY8H5Pq*>`Qk2@7wnuW!`%vSNkUV{lC;5NA_Ovsy<(R z@3PTj|J2wgypPhi?24`b`?dSd@80X#_VTx_pTGJy?a9stfAy=of)1$LJyCWD_`R%0 z{(=3!kNrRSBTA*VOkRHL*|Xzv)o+w8KR?o*@+ZMfVc{gR9{D{pw6vD>tlD#N(PfTv zN7z&L^3STU-Pf@1V{iTvkAq=RH3$CwJYRp#S^nyNg(KTQjox{;R{wj{b-{~8WmUa_ zcI{QR8l?;&`xL|QI2P8VTdRIN=yGtJ<GrR|`rXIIeMiNjLw22%SFV(h&-z-Yxgtj* zaRsmWoei62*t>aI>|3Ce8}0eGHE>FL_sh;KXT^_C<D&nqy_Bfp=WqM<O4w!lqm@V4 zeL}KUS{(na;-l=l^KqYbSlM}b<wyzn15>lFzdGutbA_!&sW4`xo^b8)>Gez0lesqi zII3~Z@{sPtgkJq?x5O6qZfkPi$~dLsedwfX{B}PQ;vVTS$$zZtzHt6Xr*L$gV^`s+ zI{D-J!G^t|OONVLoH(if^ZL@h*K)VRmj@nxVSWFn-`)6Wi_ZUew95BWTj-SK-JjPj ziBP^FbU9YG<bq@OA0gTO1!3yB+Q+5(x9*#)?tf1B;m4>Yx0C01WQNztCC~YxYUQOF zHs3u*K7N^aY)PQa+S^JNB8od+iS1W8Ha+i;&athl|6G!AT;%cO*p~FGny)SPeOUD` zb;YjzoA$0c9(C(ElbyGAMm_dSs^91(q4Ms%$lb104}z<it(}+)^*8L*(kkTFVA8(& zzrN1>oPORa_l8A9`3u(8K9Y+I`Lsg(LTCL8neWQ4?`p4koB#c8`T4^hfh(6>I>q<H zrC`xF`5)ozHH$19&)f-&6<t!4v$em_U%koa;l+tzvJY0|#Ops0Ha2NVELEL$>f6=+ z!u}W9@jtzOy>PdW+FP~peDba(9GRg}EJ2GrT=v$ViI>*v{2geR7Oi_vJg{WZrAPOQ zzkYO`*!b#z|Ba(2kyj5+I<>D*KWBPuS!Sud#j$ns;@)gJt(R^3+wYEa-|yY2TX*c2 zwQqjBS>VQ_s5!xN^6%8Y^638Vo}vWm?mzpkuYFW1YrSJM<NO`Z!)+J)-&x%Adz1Gq ztEhwOcI;Z;?<TLAv3L1eNuxzSTx-n2q9j7)i`Vog_ce)3y>`!Z@8dq}Pb+U|-9DDQ z=l>(Qf{SY>?anC4Tl&d1^vh43b^BA(y4IcQ+!p>-X<O$5w<Y%;9jkBgTy^?z=&u)N zl9w1fZr^!H$wqMLsYl+M){2?`Ud$6(x^mBt1MB`RTzT*N-S>6n^BC5w={EdlU;Uji zKQi)u_wTuf&K$VVrWM2@D6-*5TZEuX;ChM035Qo7Jk<MEYVWZHlkQZTK9lkd5t>r} zM5<RcaO0z`NrxvTbV^-Wk+Ed)rP~*l{k>j(cFkd{>~D7I?~BjLoJ+G`KR@y9-thWs zw~n6wZM)w*-s^9ZT%_L0gFVx)-?>n!+qhVzZTDK~51G%NbK8mf1~smT_cooSZvXS- z<h1APZcKiAqb@C$bXs{=XxB-rzjaHVIvYLy_Vo0Q%%6f26#|`j?q1u={z-NA=NHp> zbJkCPJ%j&Y%|kO?)eA*An=|h=_-|gzvaV>p%AHH=RaxhL&Wk_1WYg{Vb#F^|Gfqjn z7m~OlZRXD%{lb1({AZ`i@9=#5EqkZF@v`*a?%qz}d-u)Qe0R!}?D+jtR(mTfyWfBJ zkB05T7Gbs4D@Etit@mDg`FwUt-XW{Mbpg+QJ)M^y{>yWL=E+-e-}9T&JwGgelX5v| zsrJdQQ$D}BF4T0oe|eef{5r=)KR>)GpJQAZdmvpc>zX3AX{Q4(xJ+c1|NYgMR-6{C z^<eTv=}Ye~uI^u0yXWPb`fHo#U9HPJb(}MD$tu6?stptWwY~di6y@}*?bcO)p%W)g z$o_18n&*Ar>zCYa-TIEdr^5FYJ$kAu`N8w)f-g7Aen-t}@c%c%vX%Xovh9-ps-nr4 z-p{hXcTV1Yd-STSCEAy^Jl`(QFZ7`^u}wMa-v#T}&!v{^^7@q)v~k&A_v<sKPw+mM zcwt)h%zX#T>I0mlR^GYNvbN}KaqazY+qV1Wm`zOx+M2d%m5JrIKZdoJgJc%p4Zq7< zk+NgspTg?>)BfF<Q+#>Xckf?}w;Sip2|whmd1S)BS1b;f^rtt8{yh9$#N&k7ZC0IG zx%@_FCxq|pdpoUK<$Kt#j@?JuE}7q2XLL1g{cnr!*Up;X-xFz(dZEFe$@Z<{SwG9C zQzmsqpVU?PF#X@7JGXbraUNLmDD>BZ-LAg3j!#t*+G)r6JLTq`*KZ#r{1OWmS#r0E z_wm&;@4s43{^L}9KXoTd%dPLvjOTsq+4aL~bMH43*YI7=ZL0S#=&@alt267Ly|09E zKBMS!>8xFcPxa28ZE^CFnyvX&tCtDqduRCl+Ou(%mVEHq%V$o13x2ig??dww1<{fF zqjNRnZ~QEHC+hUF&BbV5yw`c(>+^mbQNI>%a<+)mY^hGd_ODz&o;d40eOlG3dF%D_ z)0%8Q3v!|+Rqt(^bZV=o#`4%sjmH+CA>*^FYL`cv+gIF5WeuLF9&h({T9>Kiyl2ch z>o{ANI9(`t`(knbJQJPBwvS#M$up`K?e2fKHT&7~X6vgp%d+<}1^!)i<_p(?n&;N{ zXMQ`KY4`3ikLuLlD>Hvi@-nK}HSI&hz4N}_oVz!lvsztgWe6GzjSkXUmwB0Y`&FIk z@|>Jswp>20d3U<J=-q3l&F!6v`nS|?D7}9A^B()6_uK6!3*GR%wEOm_qtoOaeP_Sg zyfAXv%x_#-s-3s@wm(0-+H%>>8C*Zkv@XiU=#+B^C^#gPuskz<FnQm)KNtU$=rVSe zwO4O%^1pUoUU&Z^(e*3bzj!Dp)OXC8;A|Z;!JcE@s#Ep9Erj-X8Z7^N{Krq_dtWS5 zOgciIdra;9vUO?v>P*Rx3a3qUYfauRT=(f$r~15zOOEsHCjDuwJh$BTPk79w`_mV= z-<rnr<BIi)f0K*7C#e5j@iMqG@tpl%P0M@BR4+`jTU^$mS^wt#yW&54@BiLA$^M!A z5?-MXC*?P7XVxz~neBT&^;b{O-Q}??iuGsyuPfi1J;8ZKgj(&<o$a?;<t|O1eZl?L z)=T$q@AKnKSfX#zII&Lm?~=D!wtEj*ORB0bK5nwsdQH^lD&w<XGrkuuUF2^w>)P6H zlQu-SP501HbG4}28gu^3?$rB-x6DYr?R$Ff<co1ezg!Q=a~@PVbNTIlzxwLk_jg6s zU&u6nFgY;!*58F^%Y-gvg<tZFPjNf!+1QY`Vr`^4+s2IhkI(LLT*eu%YH&5LvVKWW z*!pX={i!o+(zY;Xeavw?b@Go){VKJuSugLO`8z-JvY*w<B}I1%kDK~G&(V5dtn$=} z`-e&U{6b#gbu+Y9-q5-2bJ=tLN7nt{_rCX)(qBLG{#>=M)0RYC{**X{f8Dp~CC2No zn|SVOVVhNU&hq({(AZhe&zj`#<C%GN%M#n$@=Rx6-_5*N@YW^&+DpFQlS`{uYd$@l zyvnM1%aZrE<OM({sJ`NIzFYeF;Jv!b&v@s`bMIX>qvZP8&%f_Hw|knZUw!k)nwI`g ztylAY3S`#5P~SV_$SVb1`;a1=w+F)N61E;!lS#3Dn_9$g_hlh}?*0(=lebph@-(it zIoW^eo5ftu>uNmLa`!T6{9N#6>ao1_*7DyA%TKI~yS}d0=j`J>^|u-4^E6)7WuKn< ziS_!Q73cc*#vK19?z4Gs+~vc3)@#;1xqRmI^cpSp(rEqre<VCQvL;Mh75mfh=9W0d ztt^*69lgIU>dO1#g*W}mZ|#i_+GadI+jqA9`n39DL(va2xm|B#Oss-3!hshYXVMR( z2Tc7UzwQ47^IAbk-vxI+9pB4e^MLub{?sp5xBRcLRGd~eA?ebgx$2x7OMV}+`_RaL z<*MJU?z(5j^H=_7*jK4rFED?i?Ioc)4~vy|FWy_bZPL?A{WI+M<b|)WeAJ<=<~Hrd zCC2$Y-zQJ-``y2(I3|<Rb8EZZY55c7?Vr68l$482{%(2s-TOeo1hd!!>Wr&dJfseP zd9I>!iO)X8e($P3Pi_?F9p3UK^7$`SnJavGrQ0Veo)&7z`kq@h{p9C&1(83UKeTk* z(iJVcb(C{g<J@nXD(Bd}2&v7CTBW)5_q5%i*+x}Im)m_^X|>?Wn#<X<{_We-|BE%S zYVG8UF}JQ(KbmOYmA^e^wn+5}4Q*fV#GW$7#%Et0mp8N21kHT);V}RFEhjJgS+D&U zGh>~8@$$K4tD1A(&N;Az*Z1R-==;B}rCr}4eIs-IJ#B$2-#i6YM%SkAI^bu-vhvC0 zEh)-Dk=g5JH=Liah}-nR<jW`6%TGOg>TD<b`mNvRmwv`Ck1u(??AHY!E#2FBw>BhB zsF|_-_|Dxw9(BJqN$g$F@C0@iz~;R-{U+b=vD{kb?XWER+=lQ2%cZ$8@)!TMkiYTP zJb8bE%k|awKRDifYkvOoVog=<*{^JVHl3TEwA;$F$yhslT~64_cB@^VPHC&p^Z%|8 zf6mj?TsPk5u1m3T{)Ei+4gPI6vedJe-}c+C<vp$b=jnL6m)sibOPMu(E}E6K=Iolu z*Y@f~Ke9Vlz20_9#Jb2ykDJ?eo5^Lq|NrcGYPHY#L}tUC^FOgpJ>R|Tsk`rXE&uE0 z-{ar?{>I;!_BZbGp981(+n0T{I;S@Cj?QHHE>8We-hO7EfB6cxWdDEh^J9O#O{T_< zpPycp*AQxp^ducnuYWT~!F9XpoymP4%DkWdR&W+9)qbyHGv$Nx_NDfU54+0dyE@st z)IM`UdgZc9TfS^popD85WYWJ$Wu*)MTr4=`oEdHP;Ij6PD>B9FA2wfN|Gs6pZSIOB z{<Gh9U4F@Amp;Gt+Y-CGE8EJw?%x581a-#ldFh+K%6;C<io^!?`Tzesf6c@u*|>~T z=WCSib@f{RjVCVT{Ehmx`PS6dPye#SeNI%pRL<=;nH&)DYsIgvUp|8tg$QrHeEe$N zyoP|?9FFgfwzynMpK(Z#U9KYG@9|%j>wavzeXsg`tWlO<TmS6SWp6ef_gf`5WkQ@& zu5m6X4R)@KWl;W|rDL=wdd~M-+3RoFiS9Q#6Tdt4%Az~YhqvuHUR?L~n(VZ-w)%?O z$}YB^Ijx~_He1qf?cMf!bMM~^`)sl8?ZVH?b}ygxwQVlf4<5~J6PWGZ27BJpPrkPF zP3)y9D?5ztwmY4=_q2OQJnQtl$Tj~%d3mSKuD&o$!E5&<Zzk5eTb?Yq{c3B>smrP> zF6ZrSQrydA=NZT+)pL@+Vb8(Y)8qwLhF!}2DR5HU_p*>(R{!Ped+aV}Uf&#lbkWLf z^AlWt&pS6`uB1@*GTrzUZ@KIYd*5!*wO?`Td-k5&@*Owh%o`@@H<ZoVdEf5&YdgO~ zHlCN4srvrD_kCaaCGkw-{27^tY&qAO=ytyR6Ct~-^!?fWe@^dTb9`@3{^HlS^-o-j zTmE;4^_Q^qUg39t>x*{Z`*m&mHgn_G$3H(cea)Pk>u_Ls>qAAXgYm~45=5Bo{%Xzp z!quO$r73P|)~@N_RP`%QE}6(u6&m+)-R0VO2mF4`oOO-wt;u@R%W;VdSPU*b-=WHE z_mbKB#mZR4m6pqVE~`&gTo%N;^!BCav;W=y!&vgGD9va|;QH(@>;L~+|Ftr`o>PAB zW7+pJZhXz#{WeVK#jC4EkG!olCf6PR_91ESwahs0iETW(SJ(1P-FJs2DQrzd;P=(3 zOM1$>9e>5%R&Fx+eEI&|x-CAJh4fDTy)F`xF|GRF=lS+4x%{qW{{L9FImY-c-@&WA zwyH-auQGVM?YLa^mD=W&23Pa$ayieOz98f`|DW5Z{l8kYX|9_1dcwXdU&Nosznja_ zdCNKJtos3V(VU=@%WfMT@^WK4RlPd1vOjgk_MU*td7Rd&&kcM|bEy3Mld;|6`|kY9 z>*qL0zn#Vv<SV+Sh5hrtEcSEDcDvnOcv7h2m0WOjy>YG11LGwz);xw!R+Yba^t0)N z_>F_?@-fq2oQh~ZD`5YY+1qk^$+z@~C1s~Kah^9Z_qkWF^W<&w((0de{g0>fKVJ5< z_kv#S^*wf1P2#IOf2LW#&73srt>TeQy^c(Fmd~f}`?8dIzIR>27H5vVOm?yBWlkF? zF4D1&ea+`N?W2nQ&y)Us+ixxT`LoGq(auay?Xt_gpti@|27e~syQ{pKbCydxcy=C= z|M#JNa@bq8fS{dEuLS$g-Qx4z^ZTxxlfQRJ$DQ2x<nJT_?_;GszI)nh`Bh8jXa)J6 z{;~Wp%iB$HmzKZPKi1>;^Y8om{Wnu=-~QsO)}7pv*YA8&`@!cHb(@89(c5kouGg|Z zyJx1~^V8YOj$hBVWt<<U6^C^d+JU*@33kkOFJF7_NZECb*Ja83WwJ*q9tqEnxcB3j zdET~}JyMer{C4w7eRvrbTKRQ#{N70Qn(Ezq<(~H+v;X_j-~Y@j(><Za#<k*a>o)C~ zS2z93;=Gd${#~3)cAW^(UD!Tj`)ZM|QFh8JQd}l~dChZc8TSvB662it$(!HTeRnn9 zZ|nO%<QKzj-P&!EcB@}bKDX!2deCsOY)s~~vn$Lvl}eq1G*c%2d$>%s<l<_F1Fz%0 z@A`C9ziwmX8o8s(ta)x7IrGYKdzJ6@OK0T&f0Un|U-x<T+t62k)Bf*`GEbONZGY61 zE%#;o`4={v8^3TheZA%PZn<vkLG|Chf(>OGG#1Vad7UZlmeHsGd}7_{Z=Y69yRvrP zWu5!^RS#RmuN~gL_4)IARUew=@6=^JDs0>STI|CK+x_S5e*5Tr-f`SE@6ro*Nu_nW z{WceTV&i1r`n$IHyxI!$>uW3X&y?pa{W)*e>(4Xus=KwW@Ltw6x%%g4+pJd+X~z0b z4}3aU{A8kg-nWC(r^K)7f8v&NYgOoKuTziCmU;)MHMZC3D$aA}x80(5b!tOU&<f8e zzFTaZKU5CR6gaRTcS3{zcCP>@ho4TTO4i*<Y?lr5vv}CD^H$8>xql)~_BYJj=evt> ze$B+%56$v2za)}N@BhA6p0pP<jyrp)`EJH3j!br+Yvt7ornzM8pQe9@Cw|(L3?`qq zmumlB^NidTTXu8l{Wp?(=gIH7D?iEjWA1yK_!&PJWk;=V2>4!i^1oq*SG?Klvt?HC zahq+=efz~^m&@Cab=6A2bMXu-zu?~Fb<w{vZ`JqAJ@@d>!M7W7?U?L9iz?K6Ehb!W zIV5*@i-?iO{-Dd3?oSaEtJ*sM&lB~hmFKJPEr|r(Yw+rnL3zmmb@r)U2B39lw!gfB zC#GFGxNY8&LuWS6|NG{e_Fs8F@eiH_uY>3Rx^jfA==>J<doyNSeC)`i%bDSGUFeNR zeCh8!%cL%8B+h9mx~2Q~CTHT7=wI`Mt1_dw44&+JwM}GO#r@y+%s&>bVtjh*d-mOG z<Jy?F3W4SOHLXBPqZjAzFXPJG^W66R%p0*x-kfiEF6Hg}`Rv!ew_!Y4>u-lF%=+$o zKK#IQ$@3M*mi+tQ%x9Hx>ffWh8=4Duhwu9|_3h=A^3P*FN#1-Ncc%6V@6M;26`Xf{ z&5HkVNL;PgY4Yo-1rfUs1=vY{lDxQuXX5p}?6WH+uP)oYUSwHDf6Zkb>)R_E&QEA6 zpVr`Cd`t4O{^1nW50~@qc4__$c=^>WYrXO0<DO1+@2l_M&bfMLhGy$D{%<_4rkBgy zS!+HyPL-GS-Ti9S>R<aF&Q5KctCo1PhuPxIu3+8&v##%1CdXW~D)rad*KB5=p3W~c zZqL1&IQ0|jjlV&&JsDE>y?;EXyh-n;qiL*x?!r}5&U+`BJipC%!?ezOn$F7ABBhs4 zoc%M?ubOpZ+N0a|{wpxfzmx5G&|KGkNf1-fx?43>XRjKjTQk`eugr`_U0DiUa})7{ ziOEiSec*O3ueJ-GqVp4@eoejy8Z%jA6uNcJ`kJTOs<ooN-us=beg^Md5q@B~ZHxbA zzh#oQUe5pjbG~}qDGm0srf0i<K7LkKI&EcNW|Hz+_LX^)CQ6*M{eI_H{J$4_qi?Do zZ25BX?T*KNYfY=|=jc7SEc;AH*=WhC^D*1z_;0^&d;RT)!~E}e&TfhN<o@;T0k8E6 z9$L#%u1sF`l>g4-ySbnhg0Gsy&%WlYSaQl|eQN#p-S@B7ZhFhaxxV&o_UVns<!-Nu z`<}e*{q8SMO%Lz&)wxpjf&c#p{-<-_*E~O>z4FKx|Nme7h0iaqy1*;El;4PPNxS`@ zhpGGjep}ss*qcM``1F0pmz@ni@H}*VRj92^#OC|&3ewKLKD))*B>q>Za76a@mM=lE z+DC<h4m?>h*>9EmuT1^VH;&tFle@RB@~x(SrI#&B#OFP>Z`*DJ#lBdx`COKM(EG)I zZ^c!=HC<^HQT^~vx@6v(yFw;+%J0|S)<1hPnQ^|N<ZgKr=XLuO#LrvwzHLe5+Z64o zJom#}{T($8yixD6S4}?0ewDxOL$hc16Xxe2_p?hG8)@#XS^3oY@3iQ=n{RxJ+*xaO z9Zo;G>bIlG%O&X_R8-$hO397VNiw)SjeiShb+Oy*muxkkoEOdgAX~E7(`8%mG)?Cl ze$naH#+(bzJg>cDcfDpKsQb3&u$`o_iM`OWtd)zVZtz~e@YAGkIn$5E?5+B`MBX?! zcl-T4J>HeaB-4Fn*$UtP#>U3^qon=(ss8(*`JK0C&%7#}^Zayn82hcr$OFrnA8BH3 zUcGz5<SUVKNBf70!o`2h@0s~55`s*pJu~w-q3p_XD^BvO(qBW#bDBqaXHDDJ6q1(7 zzxVQ)(=Sxt{+p<Cna6i(ZHh_nH~Ez1%=QZ#^Io%wJhOUm`ELBLk3s@3`xaf)Z@3a$ zzpKIjSJth+vtHiLVxO_=_Ux0N<3uZkrg$-S#OeM$$oXT7vzg?vm9e@Sf3x21yZnV) zP2|$*RFCB>3AY*-)`d<864Y2YYnkev$#ti{*?wIt|96GlvDJ}kmtz*UL|1KPxo%Vv zdpYQaBI6v#sHS{L*(I6vU)<|M;=U{rH}zLv<}O=$WywY7XW9=Ye^1l$H`P6Av+vJS zeg9Q`m(QI3t<7q`Zr`t0&zAn0wXOZdt}mCoU%$S!@7vyc-IZ%?lTPwkzX@nB6S|YV zJ?3*u<GQ`yZgozt=GC2U_wJoR%Skhpo~yZcU(cM_xv8%F{@t%>pJEQAdu+8%KA_H` zsXp~mvhV2v$G3+3!4K8tSNWc9e=%*vi^*@FGi$`xyq%VkqE-Ebcg}I&73wc<E>oR3 zt0?O&q*S`<m-%dlP+G?Hnc4EsedLa>_?vilZ<_JVXF1njveg95&0O_S>-Ssz_ys>V zUfA+yhIi=fGEj+R_x|y>joakYJGRe>uAA~K-tIv)Yn{-wQ{uV5PpjVcJXGCp`)$UJ zlzkDq92a!X++1NGeE-`!P>RtuRb`!h<V(!9pPy!(P8Rzx)7$qp){QI&4v7nVxLMJ= z>+rKDSK~rsg-&@|EYEz~Wg)5cd`5EL%#er6!Yh>i_DUA1yRyuhHqrRrhc<H)fA8&5 zXF#KbRXf&~?LP<Fhgr2_N!gEtZH)6n{$zgVf7*Y7-|okQ&c~;kzbL0#tSLL8xv<Nb z-?rpZ(cXDJ;vaUt*>w7qgRAwQOZqlnUi#PXlJi<~^L(=0=NbDJPD`8VbNTt~*O&4n z^Ry4=ea*V*|NB<T%skP#{WVXNpX%mUo<8zQ^4?n8N2iYT`0@u8C>@-<Y-#;7X89iu zo~F~6saAZt^W1LxpJ(R#GvihrR($(={mhS-rtd4c^r2i*PVaC*srxgJ`FX!8Zht>B z|If+!Q@+gRy%h9xMQn$%;_@;Dwc95;6hW(q)pPUqG1+PQO~}5zuRZvVV(rtrV)O4r zzn$g~`YT}ev?d`?qwVvSdEZu@o2+=+$R^_U*Hr!LNY5YZ?*^qToU3`_x8--g>MIj< z=6?@kO8&VfBWjfjxHnSbTWp(rw@$z8{n@m?prKKol{*BO>{i~pC>O0KzwWL6d`172 zaW<-F<bIxT_ggcIOX+S<_VPJJUXdG<jvg`MOx$8`qn38&k)z#<PY<u>d0t)C<o)~X zjd}NN`cB^e_x8S-|Ld2sq90D4``o<PcF&C%c0;2*QNIuC#BX~$<&{cDWz{9#A4}du zuh-x4l21KZ33Nk|aQD;ZPwDo*Zyxz<Gb58B)$ZM6Pj;)blUw@U)&Ku}z4J*+?JeD> zoc{KIzkKsn)%;L)%iR9g?$8ZtYYh&o8-L9$Gxs>4uKDI$|5RPUpxOHCpY3^`r1ij9 zylWfQD}oy@3eGs+;D3mXGsx$<+{X_0DLiMdY-wROV)D3L6}sod9+o(vj*RUgS|{Zl z{I6enUis(aasO3*lk7kEyUuI3`?9c<vEh$n1rx*d`1-#`o*CI(d>#M)SLbudUm90~ zZ|TZ@U3p`YK|tFMFM%a@3+LAz3rWrVKe4n+w&Xv9?O*wuVZX{Rl}~-RX4l^>n|XgM z@!x3c<i@1V>0~0B9Q0MCKH>kn^8MA1M2ch2+x>pCtLx&C?piq?@ee1n>c589o38wI zc2(%=S9K>tE}q=CZpWjpqGvPH{kYPXM0viOc(?5K+Vk_&_s^+bQOj#&e%=1>M_>8u zcby@rp7qJsEw5JITG+$lVWbmwIp~bi_0{pekLuS28p(w0R`r<rTUYtPlQ_TqNz2|| zTi@lqZMxm}G6SvJ`<#+@U&no4#jL-wZ%TpZ@0g-mVS`Ne?pwcK+nxKpy4FYY(M0=- z^M=dqZ5J-RbN_9F?>57D#`$knM}O5=|Fl+k{+Gh#@z&4hWZyW)@T5w0#WlW*OFtaH z=q?|-|NrK>Tj~<GJrPg3vn5AeaemFTr-e=}Cxy3spSj@9{Xb{#o2>SkwX7&~H-}f+ zvwdexJ@)5uO<5AZ+t$kO*N)G2C(^G^%V4r|UZvO4;D2w)${m-t)Oe))SR<XcV_{|H zi_CJ5B{hnwAs6%RYT5rx@DR$=i=MPW^YGOzQ;f5xf5?h^Z~B%kZqpfA$q$~djGaqO zb>p|J%rFjLE1ltGx_D0hZ|~%F-%MYWftvOY)CzB{+hTHen~+lV@7wq7G=I*he!pt< zx>;LZ`7mpIE|mATy1+R9iM#*3cK*r}&T7{@E*W3n@>g}T{CrL6>swT&-G2IP*7bDj zxhwNG-*;rPYnLfHv1H3T(8BS?)BI+?XXaVO$6b~^Zt$74rtRV;9jw)=TRf=W*qpUg zd7tg9<25WbML9ngKl{cZc3Jkl-tIR+{I92GUJkmecD3@-dxH#b);{rlA6h4i{XbW} zFZln%uS>+O{e{Xrf2@$znVEY)o$cl1ci(=h225ZG`c-(D{d(SBty$2CH_ryGzuWFk z@NnYhG%x-AcGKU-eb(#dlw9(B8|(Yi;_tGT?AhUVC%K~Y_m<jz^7-YYzw5;!&%-{Z z(r0WZEnGRP>~`+<sVt_mE`}Jz+y)J$U20LB^>o)|mcv57UTkY|y!E&E?4K{m`(OIz zXU)qq^EGT^nctSlIcJH{nFYG@#orc$Z@#zfc3yONnb-cDcc(>oJH0PIGk!2Rx#HL5 z`Ef=fw-)Fs9%!knVm%_1;q^jSbZSthwkyjm#RX~doRPNwf1Y12JL~$gS<B+B5)Z^3 z&P&}}Gi`}h!1{|JzwRo(>^}GW{IsmIwzl_oPJLFL<vlli|69FP_d2A1ot1g=^=I3w zQlFiXlSEF>-jeBgaO?4x*>UnEv-lT%6OZ550@~x^A7vY#^>#|~sl!_*@0u(&@8RS> z0Uk{oGd&&Da;-ePSr`4ek+I#{=)vToc*)tDGk-F8H|1ZLrhlTPWmZZ2r5&2y*J}(@ ztvMr8{~TEMbl;Ds`t>($=6=sKe=wO@;@dr+SB9MDmOb4wdCi-D9Wx;%VNK$;{UM)J z%}%)c*X1nv6kL7x&jt_6lgmoqf368EZEwz=Hur4FvVCXm*g1bZF<$;#`NfrB|Fx4p zpDkN9TdwlS#L35!|2{L{9~u59>l#th0w%J{?=CnTGv^??{f~pc_*b{zy#^}J3)K`R zc1d0CRhguvzq0L;+|LD;=S4I<7A&-OaxvaNQ;lO{mJH{QCH2d1|9f-(U(PA}E$2L# zR^FJn%G!5!?g91No--5|ujRS4e@a6jm-!c~f6r#zy6nhg7il-OFUi8gL%FtEvQlN< z;VnInK0UAh_k4D1&9Yl=|8`hAD{PI{TDio2V<zYCFRM>o(wUt3OaAitX<oXQx2D^^ zj(i%rzUu0f=QG|u{X6G^$M*cqyZ?S+e7^5>*q&E<*K6n19slC||9Jf$_wum6HjESQ zSiej^zcuex4O`8o^C4S3A7oD0_b){Ml;L)6MU&&UZ%k^JpOh(C?OeKja#ozC)kXa~ zmv!cU_HgX3|Gm9FJMNX`*{^5z-dR_3t@vQ%K8**)Di54Eemt45Te0=d0!`m*zS(?5 zRlX}Xe%mA|rg4~gYjm4nmb#*_$w}W^zIofOXWIKcU09Z7ys&l6{eMZP{`~!RJ3ndb zl&!y~*Z-W}`TVcklhco`uCM#L#3(27TOZGl6q$41#6A1emp(A!xu~#Kdc%*O=j->~ z<ln%PKcyw<@+a2W0s*(T%)FjmbtTZ<*Gu5p>GjfYUTuxrf4z#erl|f2ckKlErJkN@ zELW%8JzY1c@7=V(Gp}Rk|2g4dk+RE7{(IqeuYhBHIrWWxr9GUPsz2{+nQ|q&|Mg6v z4$0&#oOP)?6SvL(`bl}SX6wvqReQhPp08*AGCOYTaX{U1*`DX$@0Q<B+dAnXqwp`u zCvxY0{oIt_F1%ZD`_E6aEE+8<6do9}cbH+Xfdz#GKHU71wDkSwwfVK#Ki`$_j}BWO zXPephr8FgCT}a{0!sD{)XS3~3%1_+?zV^K~Xkfx@^|Q&jG9?!rRh@sO9BHi6{VTds zYf|F=TYopKT=sd!ahqjw=QJ1Vo+&+`eth!2^(`j*K22ReWsdiD)fp4ARDUMFNNh4~ zW~<q>{|ozad9B5sORBuK%T~QuSjyP;MSJW2hP}J*gq+vk|L4;#uHbdSw|DII+gAJI zVf*#xw_jH+tzRK~$&*3Dae-qrQ|QiPd(Rm@oXoN_=wil&X$BteI=@tZ&Enj&<bw0B z^XZq2j)ed6FFtp+Abw?q$ZYXF4_HeDyY5K8{de*w-w%`LQ@{KFI&u5(E7Q+K_n%qq z{jyYlZpfV_Gwm;Du1o&4d~*4VedR{0yqqd_+HpSTy(F~CD?#U|f7RW^FJJr2&-=s} zy^He)kLI2LCOgk0HJ^)lKN~zwt@*8bJ#N`=3-jnnZIy@3b(bzPdJ?`n$j4Y;aTaK8 zd1cgoO`|VYO6Gp7T%2;}a9QcRIMD{9bxFtaa-t5|a{lzWK8efs>GZta%FK3_)85!W zmNnD0cbmjCS5IW>2iGS4xBOFU9P0O)rX_5fso=xJxAM=WEpew!Ki|6iMwxMS*$fqV zpXYPS<El3<lzTq!%-);1nvK_Goj30G**By3Z*bw#%%2<17?vGYpJ_jv;ncK!{r#5D zW_((rZoAU5ch{Vh&yQ;!9hKw%q->Hr%S<<3pt8Q~e*E3H>N?Z)7w#!vp6y@vY4Yn! zrDuzF-rFm5X7k6h+t@jOr0nELo_>DzxBh+owQrK2uGxI<)Emv{QwrPaeNtx}QptGF zv0jtE<it!SJLjfI?0cn^^)7r-<oR^`l5*WDW2-+b4xo)$yOd7Z{eBb7A8hKgK-JoJ z{%KIp*XqE;e;4<DyOq5nz{iQ{W%t?rUvl5<d_K?EYWmDIQyAQTNqpAtH}(ntXIg*8 zc5?2yl(`{Borz6SXAQUfmQ3Gt_M6qq$NL^Mai2<^9vk-m^oO9cbwzi}@0MQw_Gq%4 z`iHCQOr|KfU6bARwEvy4Oh}*dcTXl`<C}h)@3Z&@9Z?KTHTn|475n7h%vUk9O4(;F z^Ve~{xU@g#|KIQWFDJ*ZczelPf30@b)4g*(_dDA>`y}^jUiG_$Kc3Gzrgh!8vbD@? z(e860=Yywe9{)di{+}i9_WOAl+@5p(`=W;PA>k{I?`akKaB_Osq@1@OJowDx3p7(f zljuH?hN_JdmRJTKRFzkIYj$;l*Ut|=rIX6iYwn4>uYF(ryDe<f-w>HB`RscRo@ckV z%bhY*xifi!`{hk{GMCRa(~P}w=CsMnTRof0zg%>`ete39$L>FOWMw~iX0BhNcw2ti z&r7Fwbo_0*bc#Ld-R8aD?^Qnv`Mg;9_RFa`K2{dV5@xz-lIwIeuh-eL6vu4Ltgn0$ z^<-Ac-<jL$80Y7_ceno)I9KJZsLW~GNmEMg7;ftxT(HEddh1P=fNfuD=ji{*%RDgU zz?lz$f1~bQ);wGe9*~__7uxx0>0~Xtw;@&Q;wSCWw7j`G^>^j^EzRzcdMCez<h|N< z^G8}VsEEiZx7fXBS|XEh*y~dM=P^M$wUT#kv+uV1|MUEHRqf4nyU(_VSJ!U_O?*T< zzQ%fdc)}JImKwL$RhG9}RVO^~_;+A$CTH)H{*$Im<!+3O^A*>>a^n2)WP|aXz*V6t ziMMh+Ez&erl>AbcoRQ#hDPQtb+V;C;+LFsOUYAE-N|St+v1-Ec&-1?T*;VCLeU~Lb zNNd-VN#0&|F2Y+pzs_Xlx7lFxNU;A#PW)dd7t3QIIho7v6zOQrpPFUIX^?u`)9cxu z%O~%L?DQ<1{YGE)<E%1w_L?FYPDK@ut9hQ^m#Rbw)p<@<`#O7r?<!^4vKxt$g$~aw zKc%_+%E@!z%w`?u<X`$Ed|mc#)wMIL6|SpzUT;}g^tbq}{D%g1pIh!u=906GSKhw4 zCGUW`V(%1-?4Sud<%|2D?kUzSUOVec_Nm)jqx)WdU0+{2*CKcSJ)3#C45{Atj&sgc zxiYOdKdM+>y=Llnk5z9|cHa5yS#I}jWB*){SEp*{W%>VQvfjI@;_1F$_rC87U*1)G z_w&B~>_bVK(Kq&l&r0^HJP`8Wl_}ecoZ5EIA8$4s<|}1g^vo`^Pv7%PkNm9Ps^5#5 zd^wjdJr(@D&i|9y{MfzcRBzwD6nEC}{LzpD>3Sl$&8VA=LCYc-7?~C;D?Bi^x_)Y+ zo=szq(j_^?$Cl?S{VJbo$M5{WcSTTe1~VxB1jU06sJGAKd(ZQ9OV-s@isw^4A3bmT z{mvwL0ZuWWt3o_pUl+7wrd=}POFfqP=6cXk)q``&Qv2#oo2`#8x#$WSNbru0%~;4< z^GVISbk^^?@9WM!<TIVS?vuCKv;&v5<34st-euo;ul{>^efV^B=~oA;bnfu}sQL3) ze!a8W$_qw5-dCrE<auQ*j-L$*(EG=CzLSj8e)_a(D@)BLpWVBHH^&@u;Jl}D<J$VC ze?M+nZo8I`?Q3+}s_VjAPR&?(w@`g{Y1(URp9AW@ZGJpxetjx*Z&UkO!>_ZK#U$13 zFir0H7M15+{q@#!pLpR9Ggr>}3R*1HfF585vzVCd7FXVt`FcpeL;mIVeP7phHhccR z?kx9dLg#ZifgQ%296xv}O~s=QsJG95*lXky{4e#z+QjH%T{fT37<)6>HE-i-{(2_e zZu7cx#u)<7Z)LC7m3&{YDzR(U-{nT<<^LR%_X(-XjC*hDdvp?LZHlUByiw0pC+;6l zW+Z=^vfO6rx+j<AoYq|bX|jH4m1o<{Qy0RTx3uh;_y5mxe$hQ%Ul&YS0ouXFcJt_# z(_grLq@36h!h33u<00wmF~z>NPoj@4nzQPxS;M5<{FlEirmmRx_f7is=k`~3Zh8K+ z_R~rKx=rgkuGa0n|9DS$Q9f_d>|7O_safHix4ugKTXpk%PHj8ykCgo@O{P@u_$!y! zxBR-v)v8a;y7S{Dqe~`iTe^qs=Jy4sIezeHrCMTLn|#NTg{9`5%=L?rcMO@{*L~lu z{#Wpacig8*zxv<2pRssjI8$_(!+~^<`>hfmGW`@C=PUQyY_j=vv45V)kx<ZF-tmgI zTrbtRpxw#Ad?&BQt^Zwd-uAnWyL@fQ&hLBQ$A+zoxj8r1{=7gU=&+m9=KDVOYBICl zof<q>L)6RY%*97XyLWGX-DMpA^HjJR>twO41IsOc@J##)n%BJZec$)DmAyebpMtu& zA6|vWF1;DIK2wYJshmga(zwJ$Z<Iy*L2Hb!9d7x0>zGgcYw-^!zZJccT&3eY;c494 zZF9bt@B2LWQ>%W>LcZ+M=jHRyn>AdzcW>IB{>}4Q!uu?r&G=P&X?2qSw39#QmtKo3 zO-Nn!z<1i)Z9Jc+eot%)DSUS|=&st~J4el(%YCn>y|$j*;NPd2xPxc;)P;YI+_Khx zuRBy@|NrOt*9sx4zVqDP)^dHraf7R$nCyaI&Tz*_s}2nejn?xU8qUvsKg&v)iG?GF z6O@rd#fuIsuhrrVSh9Hk&*>k0o6JC)CwD&T(tdT^n6rP&_ltFN{C@2Hc5Ahqk<Q%b zFB|+>XT8yO7CgHw_4e*}yRto(m!DI1?8vd6>gTpq^~i*PlYZ83Lmr7Po)i3!SNh$H z^bIvXKmD4ur0Wc*Qwut|qv+?;>DRv>C^@|0>+NSH>-|MPWL}HimJ?~a<i<Jc_c7sf zyq-?IGaYm-)8-ShW_KrjFZ9#8?Z0&Coa%eCcfPCaKXdf=635%Q+wZ>m?)`4{mK^Kr zwHLU|&z6ZDGI{@fkAD&W^SX1@|F`e|`*zO0@9%x$xF624WW$>L1SH}d4y0EYXM+|( z%CNH3wC#DdSm=Y~r!6{u34SbWoIlQ-?8|9E&-L&X!v*Ss0v~oxj`3LT*uc=JtE2G1 zSY<=Q`3VVP^4g4y^Ed9g<A?JOb8s<g>9B>9iG`!5v7zDo1o6lN=^h5%{Zktn&PT-N zspGtm^$bY4jKVDzMkbbH4h{#>Pju@%Fjkpy^thM9f%G+c+b65SoruPG22#!^klVz- z$ds(0@WA+qtJnugp)-$?RTUl>hv7SJuNb799o(a7G!zi{Ao)p&`v(ta+Q*GT0v{w- zpMINyby60T>?{QYKFnlxnXTZ^a6p0sR3c7ft!ZP~{3C*s<44ccr)8V4&e(yJ%W!b~ zIK#Ne7ZgK0ET9-V0dCUPtYO)9J8$=<nxCIKi{tiImG1ocY_|HmYBc{Mm)ZuA4hPaV zwWovZx)AMfVEK0M`{ArLzb?(d+DU^ALLHdE1hx^>h)&i51zD>&ILJWe;I!_GsKAGp z-pOnrQ$S6jD2Zkpkq?qCCynrG<55z0VB9e&736ME#8x|i(*)Kg7A%Kf03GV1-zy0+ z_=1qYhs;EtPXPzg1yoN%YjltVrb7)n4rDH$8z!b77jtb*r16PI(?Cn%8x2K04y6D6 z_4Vmd{ko5PxJ(?cRNl?py4h8FmD%jMTtCjZo!^DsJg5y@SpDpNKH2pBUiI|S*VnGf zZhu=T2X=lVhYH88l#`Q+J{}dHzW+z}{u$vSjPom>ZZpQ7H=)Zm#TFe9be9R7_w!8p zwRW{9n+4WSNLF%aI3TNip{2S=1hlN>^!0t;wpQ9I5M$b^1Dxt}0{E=oY-p{j=<5b$ zZLx*}me1z|-}`;<`?=o}Iewh6-YJXs(8&z}tU)KAKjg2kxLJJO_VvODu~&swdLUoS zuy6eO`ucQl{k>aie&4=7@B1m+vyogsOoW!<4Kr9gi7jYoIM1|4gRc|hE>IeP#W6H+ zVG6-g4$2A-E_=+t>r|M&hz2GmJMIsQEL1?wga$3x6s$J&HZ=GvCwk$t9i}gVk&EMp z&VlYn6L1@c<arY&CcEAi>=UPG1~js2C_LEApx@<;b@B}60GJ~$aBz$UG$IH`0~(ev zz<FRaMI&eWAzE}OOMlq;Y*zNHVgsBF97F~XP$*a~8nHF&s%%kSw^>N8S+&@QnZ5H$ zakWxFH8re!b#rw%kRGyb%3~F9;z26EU@DO8k?;n!_4d5+n}uE^VbPor0vZ>2>SgIi zL_yrRQAFT_<muDzmW(<Y6kMYrjhu-YMw16}p5+i2O&+7k10@7TlLu-Dpajln>46$J zqov1a@<0iJ5tuyO%pMqP9q5}(L_@aWfCiV&t$?k8HU-DAo)?T7d<qT;GAt}L#~x}~ zyMvl?ctRQ^)i_a9;Dh8%@8<`IY55*=bU2VMvu;x{QO)&8M#lLQ_m=R12K4Z^>_;69 ziVNh391Urtj^AkV7)>7NAuyUeP(lD@43355(qogE)oa*U0Hl@K&t7WHFZ*^j^>AB| znC`CH_TsNK&SoKEQ}>4J>K(t6|MRW&hYliYxr8UIEH%$w3hjIS+C04amc)^nr!zO% za{ka+^>6#aU-ubz=ZE&$rQEccJM}J>ks*x4k(0^DIA2$HPv-CUzkd0avd&NDpL?6j z`u@M8vx;7tzJ7Jep;U2xRz3UO(oD-&3wGW;Z)129|Lqu%3e3UX;lOg{O-twP`<%`B zBS+)7ZOYNNr8Q=}H%$Ytr?Z`}FMhl0Q}6t{VPUzuSKKMO{p(e{*3T(L|99(Z_ge+1 z&etuq&7Dp-I1J*MnCw2Q)V>POU-hk|`oQz;tFNCsIq!OS=Dhs5)zOiAc1&TH*|6l# zkL#~bo!uTbJ1*qZ!t(eqxibE#HP2XXx#39~Fsbqh4GrgQ4$l8I^=fJA+{b*;6X*E< zJnI{JKs`B2chw{d&7Hx%X}|ukUp0umzBVpiuI$66P5XY`-~YcTcW0&Vw(wJn-)&uF zlVLx#=GjZ5+eCz2!Z}b3&)M>w?f*KiA1Vc(zqE%rmxNyKWt^XN)9Qxt?w9&{F~NF! zlWHpu*lyF>{dUWenDEy2x{%hIyj`WvQ{|&V`m9o1ciy*ivfI19C!C1(3U{=_f%JLn z&fl-w$@L?IbN+JslGj!*if=Ac+%>DlzgWV!vhH90wQ2wNq~z}WzAt(z^S3*z?(E8Q zmi_ee-}!v)?f147cK?6MsD1ii`Q24Fdx@D$InV)`N4?zo>$d*=i12rn#}2Ep*POG^ zo?5$Vl3gfY`s0q+v&{$R>#r}ej?Uk_LuSwSu&4XV<HN44(Jnpl{MOa%Q~Y)NmaTdc zeCpBBr8Z9w-k!!>7H{RBo?{M4fcVo+v7!QKI|_5o<0+ekKHLnL_j5z;Dl0qbQ>m+0 zi}-#0)1!YkVr?vI&8imvxcDnlo3k%GKfUVW_H8$-Ja=A?i=N6ZyJOLs==}@VMBZ<y zxi;<nzc15@UeC%p9a{DxlK=j?H=8$4IL|Qsj1pnDG_r!6;y?Z26s;4J51c#J;J=c| zF8g)NdCB-)>(<@*yvY8>&NXX`>_W|!&s(?pkbU;*34W%x!oKD^+n)WiXWO=_INwv- zyv3$w=iLri_v3Bu(^KC0tJ_{g#mUt;h<;c(eZ7<2Y^|R$#gAucJt@xK`E*;*w6NEI zuDj>Pf4fn5(q`^XqKb(#ZVm_1&8nQ&{O;4g7jU%NtMb|5?JNI9hkn>u7%Th3@-cVl zf#tvMpYFe%8+_pTw*-GCTj7WE!fvRqf4{4yaOe3|s-+K}b3b3F_y6DP=+n*bHtvyH zl<is@^0jKMmmTNNn&-#2Pl;aTW#_#*=X`hHt&MNCY?9jUjdi;|TEs?N0F?kGmG|OU zYgWnlS3Xk~`k?vrlJsgWyU^b+p08iGe0|lkccowF^@>*Kt-o`5-J{KhA2x41b^hON zUA^6FZ%ACQd^|_`l>RC%yY~C(vx^SIPER#2eI52`=Pu5lAyaFn<!;@v=f_6tDQ~xl zeY$xh73;+j$PH2sfe6t2{O6L$d*51Lziv3cS#IYhsY$C>J=nZ!)q`MP`(Hm-zq;;U zbuLz&$?ml=OZe1FnM}5vuT-{dUbpH|akOdj^Ho*n@2!1h{od~NtxvmOUz@tzrsVdf z*L>cujc18`$_)Lq^H-bc(>cZY>$HFB+|LU9`S|~)RXWkTGUkcAX+sMhi~=9DY$1Y? zaem^_v>ktzH281r-PPnj_0Z23&)2OuWG#K_f6cqNs|7LAt3ThdTiaxJbra_|o%l^F z-t1U2<^G*L3s>FQykgBAkyDY;x_`pw?^zZ1?q~n)jXEo*WqbYb;rbb(xX|>q*Uqg` z|5j-|`MmyrUGdJXtDbBwdae3&zkT8HO`+E=!^%~hS)*~H_>Py$W}gBrjRLLAnI4_D zGqro(`7TlIQ=oG@<i4(OpPIYvrp2m@Vs{_T$cs6UK2Pt@{Te&b8nY?swpq8UsxpHQ zKHoh3`O{C!p1;4$^&@A^gUc+nK^K>wdUnNezRWwn)8bRtu68(gDXU~H_xgz7d*82S zpZ=8EeY&w)Puu+dwuN^}PX8);J&X71<F8Mrz54lU3Ukzy2j|;1t%=I}ohvro_3Ezh zm{?xnu)Xu1udlh}_gN*8aH(@(n$V7~*P>5Lrq2l!`ZV+Ww1V>+kIO|1{kZrHwC3lX zZ*|_se7o}6*p<(3mL6C>#XE1!A=9hc@;~eMuWmNauY0(!RCHcTtr~0fs^=%4Uzu97 zYG&n~GxwKC^}gP1zk6rRfAOocjaVyIRn9K5akl>-fBw$a=Y`dHZ$1MjTM5}l(2DA( zdp@7r?acS1ftgRkU8eAe?56kDzx`Nip1tJy|22L6YWH(i`?!9jc&zizj|msOvXaSe z{!iah7U@&*^DDl0HuxX1;rw++?%7JMA30U`cz^xbpC2E-?S7eO<kq{FHYL42n)>t8 z(a@>$TvH28Tdv>AdRTh3ZPUBucBP(s?=QE>>o5Ac_4O&;)z_zYuiLS9)wkfYYo7gi zpZ?a&U#QF(waJDh{p4sie&7H9Z*)z>x@R}0>%~Tet&Iu|TOSu2&g1oU-gCR}JJ04^ zwiFTgFw^<WO0!V*npHYyKlYy!h+P_UC**_W=Sx;+HeXjR*S@VA8!Y-LGK9S_FLwWu zn7Z3xSJlkz^5f>eEct2~I$_1F_nYhL*ZZ8hcUy1m_s<8{-wnuH-S+9%=H&8uuRfe5 zTw1v~Jh=P5?t7a1%!ivnn>nZ5Ex%uT%{e3W#ro6w`}eGg+`R0~{{R0{KR!D8^&jYf z>Q7hK*KGw|s4@HPeeTWT0v~4XedrKf@%sO2tsfy*OXhuk@%B~1Syf-w`Tp@iYxghQ z^YX^(Rg-Kve}tr#2kl!fm90IwG}UwGx;GJZTmR)fUB+uHzx<tddR&0~^Ud=%`Pa() z&e<MXEAzkP+pDTyv((>OT&Rp7+z5@>!F0Fb{HLd<PZu7S4Hx=!^U_<;DpBiq8uc4g z=djjnYsu=*-S^b^`nC0DjPtu!H~0(L$p5?Isv5a%f6b|%U;osJR-Jm3`gGS?e$DMV zcXvkR26FwrB3j8dci-c``&TvFod4Hc`?}%$z0d>d(myWTG{1Jc@~mO;XSUrdSFx2G z?ah8w{POF3)zE^Mde%9i)AY`9W;ApBdA7^+>9$hIIl0NVtFL})T7S`iz?5v`HV5+u z#XlalPp`iBdEQ&sXHP!PDwRIPzWU6;%IRx0cjxV2V)MQu_w>2jx-%!|-ahe+iOFvE z?3{_=|FjtAhkUS1`du}v;e6Yxdij4(SW^o_S3du5^YzfY-*ZKNL~hx!CVtnZsJvv0 zH0MQ*(YeuLy0<s##O;cW4Ep=}dR*<T^P7Kvdb9jp_<M8Fnomyma%TQ}IXV1mbm@xg z`#)UHKHY7%#c<7){G)59>1eUP*|x|g>HphPtNU&(UK6uzV@&w|r^kPOo&WFIvrkLP z|1Mgy)=pw}*}Ie9HCJzoyk%obz~78a!l3y3cw9a{Y@N@wgm+PgY~TO<#QXG+_BFqp zlBZp+Pg(nK?t8P*>qI(JDxW;Zk28wTLT3M3Z5(_sJ@~-#S#dRomo=PMKewLQ+}CrS z+J((a=h^>W`8)i#cIy1MLINw9?BZ85*<CHMW461>r+zDL^|YzMeY>MAOYY~Y$9+y% z?BHa}`CaA7<viR6ry%C37wBH#{V}KFQRmy6o9pgq-?^Mq@_ViL^y_xHd2Rw(+cI|r zf|{=HTFmQz&8qVM|MkS%)lHV62h>GtR=sSu|2SWB!Dh|#n-}V=YTIyY()szdZy3MU zz2EjX?CY22_}vTb)=jRff2aLp&p*!UY3+Awiwd64ntg5B=k<HG)_iimK1Due&#rsl z`=a$V<Ex(@Or4$^x^$(Z+27|=*8kh~=j-(PVE*G@KdicS(Y$`^$0*JpTc+BmA)Jn_ z+-lr*U>eU4AJKOw4RiMUihihE)!_fT>de(-#`!xZ%}KN7{Gqbr^PN>Pq1F#J8(z4~ zQvEFd7T=F2JK}y%*~$N7PTilnrytwD)ICm{zkJ@`-JhoE$A+DjEuJ6xH1_<ha;c*q zXV%_(pQqVx|Mj`5e4l>Rk-XG)QSVn9{6Qm~&+Q5btoo7&OYml`S;qZcXBFQU$^IML z?&RN3JuJ8BDLZI+B}1m3*iZZUyQ+c@q_<}kOP_KzU$eITR94%QQ@#5(PVf&sxct}s zX?1nmt2)*N*?qVw{p!HQ*Z&fh@3c7ksdau_bljJPpI_(tihelB#QD?a>-+QC_xEQc ztInVQ_x$f^xzoSPck7>i&YXPn(u*@|*2eB%s1s}R`RnYx&n&h@8U221ay#39wey<T ze`||w=0AV>)cgP6xTo*;SMF@^mp++HU<CJt<_1+JyUVTBMGF4aFHU`0@OS!m8#VoN zpqlqv8*}bMef!GNy?2WHS25Xz20dLQy-IP{D+Pby7c=i?YW;X(@$N)}zr(%A(2td$ z#m&P${=6bD8zHyj{3*|Q&*Mw8E&oPOpPnCjI5@WIZtl}<TcuCEySh>R$EQ7q%XIyB z`M>9KJnJU(qOx4H@8`ApdjWMdFMXd*%lgayX0LDMi@V#kkJ}|I-*mn2^eOiJ)u}%} zd|$6+y*?)D-^a?|=Bx9}Y+paWKGpyKZ_}sCr{9gKt9`1QdezMP^ug+!Sial&w?92{ ze`}ZCIRE?K*P>6)+ZB77#zbqT@A=U1X+qZTvjwr18lp93yG`P-wFHq;XrnJvvFry) zl{=q*x3AM_m))^vP4xD4I+2(5Z)oo}DQ0AxKl3*4)-)zN$yu)}-Bl-SS{O5<!T+m` zbBSukWRv4HcI(?;tm=Er9KUnly}z<m2WUVfwLieoXYg+xBzgqyL?~{7uup{=vDg zkN-SpzQ1$RoSJ7xRYh}uZFRqwy!+EX`#by7L+5Gd$3(~dIsbfm@wxpQpT8<!+2oYI z=S9P(jq-7^>=~c_RIA^u-kd6|RrUC}zAopzFWKLBzBYS0_o_fLr`*|Pv0<$<<UaS! zpDLXn8{cPNGX2xD<!@s1{SKs?t-6N4P(1L11AGj{t5Q%YIHmbpR9IQe&I+#?iHB|D z|2z^uEj<6n5zXzH&(5&0)TBK#`x5oG!C&k9710_q)*7KVl}F;V+wMIx(60G?DO#P$ z<>vi;p)*vE7rL|82tBI2yIg;rzv|WEzCF*seSNI|_ju`3R{1}re=QEqUVg8B`KnLn zPHl=d-EzY<{C=2SK<M3{=axM<ce;OvcjejS?=y}}47l+4-?#Lq;`?_mt^0eg{O&HB zuiwkG*h59j*R5)@mlV^PT3xns)we%!TkrDdUekXtnIWjjBa?sM?NguDu)h!DyOZns z^X=DcuT{TlmaO_WdE4u<=&AXC`<qXjonH2jVU75^&-3RLpR*0^+V|}^`_#8#IX-4w zKfW-2HUhT~6>t`4K0-6J9(?{f|6b6(ABShZdMY0N)ni`yk+P=;c84i5e9dTXsh;;` zLGG!;@3$^mcV}|JdDZDLL4GzLkH|%TzLdktQj?}_@!(hfuOr8}?CO8qkbRwSw)Oh@ zwaY>u&U;nA^2O$*%y##8J(=eDd-v^c-kWcK`nRgd-|NSof0^6E&q;omQl&R-!I^Hm zk~-V>Umkzg?zc~QrMM;T>qhR=lJ++&YHpVQ|8g(2U)HRyz~=eh|B>hZJ^h-RDmv}w zm+tqG?{@xv!Vtn_bH3)h{^|YyejVHN_S4a)_W%CWeVq_?)-iSV&9YmD-(y4X)xEJx z{dwu+!P)O)3z9{ZtoFwgU%pkfGmde7OGlsW-|1if2+Qrt{u%FoYxNW7b$8F%e4q8{ z5`TQa{x9}@w-*2TII%qQyxfnE;;R$RbDOu$`uFeV@2TqdewSG@&To{|^+3vin1S86 zQS62Iho29BZlBs;SN(~@!h2QT7va7;>)zB)KdCr>W?MCr{)5e_dG?I+bsz7#8D`G^ zBgDBR^#5sr*sm92LNBDp&C9=azHYYGk3IiSm&Y7X4=tJZ?fdhq&INDxpZ@geYq+e< z!?KfGw@>{V+Lb;#)%%p*?Xbipa=KHC<NkJbzq0)QNxgl_-f-R3^5K<BKW*6mFLURU zS+`H|MeA!m-}7vO7R#0Dp8em$4(I*-nXb3;`fJe-md|4MYuN94vaF~-HhhZyzCUrP zH!u0^O21q<H==cxU3}|oA&+1Gvftk=ub(Y;KWEdgZC{_B*#3V_O2@jz@3uNuzAt`n zmnq3O-%|#3YaPD8agcAUW1MeZ`{q{BW8K&0(b<_-=l*LCRh_T)#Mozty7ccm&+Wp) z-({NVJh<#}=55G<^gP?&d2jXY_7zGje;z7Y6J+?jNqW^P!%%_L<eM{XR@}0&yI&s6 zI=}wUr_!(M{O2v_vg5q}=Utm=>D76``~Tf83Kq@rbe+>bw<5vzPiU<H^S|e__wQP0 z^IN_yRR8Zqak&7ws_(h4kMCdIcCPBe?D`=4x|0ugProi#dwpN@8TR05d#k?QI@|VQ z*T-w6PZ#{J&D;BKzTLZZPgDEv?R{Tn7tQ<dtNYb}xXk&LCv8u^f4kN9eromEio?(K zbeGq^{!~@5etn2@@!PGBk8KFryoQ*y(7~KvxPSba?;jJrZhz+I2LEkSC!FV&+gy9I z__^v++xUG8V=9gwn^;iw_W6t7`N49vC*Cgp@a1;=`qI5-@X}uP!^%Ut=c{9cKdjXH z5t2E5S=0H&-}e1|CZBbfS?|wT@4UHN_Wv~!3ETSL>OrvV^^?cX{S}B^y)7~-EXQ=` zqqAGh*J_)u-}#q$u~tIJza>)FUOjd9v0Za@*Q#0mO^(;^?^zd9fBIPc|FBR7tFN#3 zuV%f)IRAZ^bH44{yVC7ft-ebBo_2r#{Z*P%`&PF-uv#wFwL1FX^W)R+hRmyYrT?n* z|D%)DQ`z6_-SoapZ^x@^pC<VpF5%sD{##q!zuU7<3$OaSX6?C;rTzP}D?cta;}_!m zvE?Yi=2;!nUncYEaeKGz`R#2!<@U8O#YVZ@)mbjHcfZ-NB&H_xf5G{{?{|0EEc<mm z@9($kamOCt<mUKs=3c=9-8~<ZS3cOh@K2)X2T#%2-0SXtY4iBEv}zxy7~An<TJQ0l znf1RzMQc7ytNkB&J@hi~)AN0IQy>3{{P&&vszIp!s<zcqOHw1&ZMS=ySF8O!Wyh+^ z`wA<6`ibuQd0spJ<!{BwwOg6(<o|A8ed_;@AIBE%w)^VNK8^j|W&0VEOE>3Sd&yQ~ zSNzUYc;Q?f^Ni|J(TJ*}6NA~O-MZ~1p}BAgb6IrYz7SsKZ6zn41cXcz{gByRNobb* zFUvBnJ25p+Z+)s?&1L)7$?osAnb(82s$Z?%mAmS1>GZEPpgxiDcIm9Mx0&r$zw)vY zkBJZcu=3yakPnd|VrO4}-W@Jl6XckYd|-J9<NTfL9~9qVF4Ovcr?#jl|Kp>eZ+9~* zcV3U(wyu4l7wh~uUFW#J-}~bXUOJ~f-E=hd>Zzrv$Jb7u@-|#&<@4J8s~%0h#`j}R zBx`Zpu9dUSxvyHdTW(wCi*<3oPiE`?KEe9*^8dQCX^iuY>k3LMuO8Q16Mnn;>ZVmv zfu$dqx1`$?JpXiGZs%<V?=a`de?PqXbjsN5-Q&ahJKQUO{JZ@%#{a-_^=@%a{8J_i zlnr7Zq^T*+uX{M}P1UZyrF`cvOLB4iI3tjE<LbSNV^c#LmR}LA+4SzGFYEk7bCcOm ziy3A!nXY^=`CI?W2f?hh|NiU%1;Ve7zo#CT590MSOWFP9*6mY)%m1voQ}Ozj)tS$; zj^EvC^KOCcsdu--4|i?7WnN$ET=~uZ&#JnmvL7tJUz&ZYzVi98g<JpqTKRiQx=qHb z50?zz|8o6#QPn(G?8DEv(9q{AXSe<Ob^Q6;cu;#|?cVce52xLa4h^lAvvL)9xoCC1 zwj`U*+Y7bZbtCQG{<lltzv=C#ECZKbLIZ~(OwBBLZJa-C{={jYnk=`a^65Ob>Ir{M zPtQ6R<8UDTS()?N%IX8@p<C~@Sv{D%aMvp?yQ`nw-|tOY@s@Y@hnrUgV&DF~yO;Av zP3B{PSZ>V+m)FhtJn#9Py*BS2$X3nETlH?qFTW`32gP^KzCN}0ensBjUmL&QU%BV+ zj@?)LtN%VfC4GHb``)k1pFT5vZO;88<=)@_pN@acQkSj0{^`=;^7mUiBlmtg5?mix zS98<X_};Po`~TUcE?>23q8q+y1mtNYgNz4iit{h8ez@z}8sDuG?&a<(@V_QiQor~6 zgR73LEH!LXXFV;-|MB7&mz}!I-sISm&;Pu>mUkzKYh%d!sqXJLr!w2c?|%Vmr~Kyg zetqD){k+oLz5ng&e;r!+;PW&6xUhSb-`>A^JXh<$r(OQ>F?Jv3YQKJZi*f#*4_j7! zI#<4$%f?$=Un_n8s|B0<O?4vHe``A*7v=Z=+taUWc87>I?JK{uS6%np1M~k&{(Spo z`gB2-$Hw#f-^@$(+`2a|_w6p;r?<2src8Tyc$)6py)or?x7R=2{&do+q>CkQmxpq^ zSiXPf&bT+<-)~=~6Tho)#|Cqm&EY36q)K77H&8`74j44f=GKdk{`YY8US~T{kKoV0 zh1$zqWz*^}=bd4yWo4=Pb?MC8p4-fJ(%L`%?2=lvdgY4EA04M|z8P{c-EQ9f+V-`p zVlK9Q>W^Q;WEUFk!8o74_WP-#OUw1v^{>8vA%gS0&Ex<7R;`KMZ|Hf0_eah9Lsy@! ztgnyT+gtr+*P=bo_ut!lr*`^nXS<Dc*Ujs~?^RvPKefyE_6yMuKS6z@`cIwRuino2 z(_(wNzWPbt*T>)LuZ29ECw%Y!>!NS^>(@7%gIXz@PH!uY4t<^*|I^n~LgVe7$?t>1 za*em&&Y$0Z9On=;BqR)WG2K0I7BWuqM*hl6(NB^lcQ#jjef4P}e{F`)wNCYU7B6?I z7lXE^oA+JLds-hZS|fDF^5MtTum1Qk*_Eewv(69UtGYiu=wQ0@&-v$Xl=|^K=eFOw zZO;qm=htsr{r&Ox)MNgeg)iRzd({<vD%t;MrR+V=fM=>#FGRTC&2BH+zKZMo(d&1j zj9RY$|K5B0^t;fbbGz5=+j_6g{J~`L_>UL4Ppj9Jl-k;d=E-fkzwPM++iiN`A<p@K zA3VDHbkF^}LI3`LduQXfe$_ttALY}|q}*5FWZC4%;T<6&%9yIVsbht!(rP7*+^tK) zws>9T>R1uFGHAl8kkrYsEFs^V1b$5QO`WKk!>Q7!)ADBW86UOJ*3bRsr3k4hDxUwW za^(BW@Mm`4?VisoeqLMo@%r<xf8{p5ZaCpuS+sloIwm{I-^C|>o<CxI*=p5una}Ue zK3cMu?dXhyce_jf&AGPo(ZfjB9qRdCW`5`GGv>R$>c-|d`&nvEt<2QL-;>YT#k88O z=G*Q3{Wt1<zrFs0-QQ<wjh?){{j8{4$@67*A7{URwYc}Qcf8*W1I2ecpU=CLSt#-$ zvU72cI+I=M;+W4)>9KR8J)bt5Kd`9iT=z=X@Kb#*D<@uN6wSL>TAp?BYUTc3%LkKH z>&x!<Sp0sr^5^4Ef$`zOJ4M6f*n3_|FPAsk!g2q}=XFeWJH5{Te7yYV&hmXyr@Ov9 zS$Wz$ue9{u4nfz}c`{P3v>r@We=cw_ef=Dfe*fI4y8OJWp{xD`@0xM_cVLwMc2@20 z0v|NDufV>EAKv5Ikjb@1>O*EuPEX<EW3K1tWsBd6jGh{^_xY~Z>$;yk>pLW~^fyb5 zm(WvNt{+pb7jIoXxAN<mMM>v8+n0*PYF2!5TK6E>>&}wLjmo>$f4f&yQy9+mqvT%x z?SEfgRTp~a&5o5hZ<F?T<%8f~)%l)R8}=2y);27>*7aYK$3L`&@7BWnCuNb7)3+a~ z`~Ne<aPR5{|IE3|<6=elePRUaE0=0MGuQ96b^Fp~TWmUip>6jo+vTy}g91e}N*U+x z@#7QwtH9ctS5;HyGxKuLjH%b|m#&`Y`g$Gz+e?AGD@yjxac7)globWu@Q7thA6gU7 zUUy&><NQC<zrDDqT%<bxU^Bb>_jj+S`$z1lC_FR6aPf*en==C4&F7q(#bh_v^wjsr z1IufsWyfT?9!O8%y0xX8>&KET{he2Q^nL}g)U7I>$81-*zdL{S<Zb!=MXWW~IzEW) zuhHFlW!98cFL++x57_dws{8bsZ`&s8*BZX7NXzSyyJGw9-=V(MbHjfn{d=@+hCy=S z7r)&7+vF|??3I4)6>mvsvtuK#R>9Aw)7^tFck!8Ct(g&{`l3bHZ^g4|^`_OFKSExH z&ab?BM#-c2Q{qn7@Kp~cN0sEBFx(Zt>cM2W2alqIS?8D7to9MDF?(|VO6B|I%XPbM z`sTW}&Wl-_UaGwPX4U)e^Lk!>j#A!!W7mTh6IaiEWd$j_;7JIBapMK+v3_~^^LD@A zuvMNjOg?tx-|zSP9lzbXVlMh&<t6Djxeq@RxPB~g@tw;3ZB5yJr3aJeJztZ2`tqXH zUUTwyo&EWE`SI_0^-E`zU;McE(S?nFckg*VCwkd2|95i(Ys$~Hw;xsierM0=N<D%h zv%T3NZ}!*M@&9+d3au&nIuDe_lan9cZhV>hy@$yzv??_{#&YqBDU-E-<oG<EzWt0} z|K)j1cFpF?jz_&upE*tIQ_X`t%ahM7+O}f-+D_%NxAMFBUyHu?_+R9`$#ZkmqfPAT z#}4c3>U=vDTDR`kmlrF%i+S;vE&>W4%EV6S?s~WP`@I=vxltzO=HXxbPd3ie-F8Fi z+s*X(Cluuu8!<82O)acFyed8B=EX&;m!(y{ou`zsY2%#yEXMhNnC8`#2Fm>Jex7{o zkm+{k+sP^Kf1KQ?oE$JeFTWyR51Op;)jVcf8XEkk-aBBv?nMT3=+d{>Dre`*ZO;t- zpvkC{6Df85Utw6x{}*p&*zDfP@w;U8>duGEQps1+dOvnvne@8Bu-5ysEGJ=Wq~|s? zoOk=~WfP#v8g=_t_q147#id?(DY=9~$f3buCJRf=DV9u0!b%w!nWhQ|d|27C%9IE< zv2aZBbU3iwX99TRH=%^A5CA$Z!fQnoQHz5|9Sv&hA`05kkVeTPqsfEv<WaWqS^fRZ z8n<pMXJv)eM6Ub%ZBM|JBO4gE79Pp#)i(P;%ozd?a`hD+1TPJ*c=*FLb9tYFpJl)0 z?a9n`rZ1P<{>?F85-nr2ZJ}5F)LcYLMu|rhUufwnJP6KwnEE>2V&BRY4~nnIJzcw@ z_<zf_8CJX3uHXp~`Q-Wgt;H5QzFQmj{0`|nX@;*@hFQ3pgX6~<rl6IV<?qjBwtH*% zZtvajD<31TZOX2Uoxi={->W&>ejL86Yq+4?S0^uW*1xA-c7pF^E(-ka3p}|z=>28< zhgE<*)0oA{@gv0ZSJUoUm;AJu>@JJxX`Vjk9X#v#zT?r0wwWb;fBWiDr}*upz~ZP~ zYDxXMQc3SGYW<wTdW)C^XS^y34}w*HaQ`b$42(>_Gw*KkIrX?)FUI+If>*1l78aeH zTT-$5dA(Qeu4j5NQq}LT2zXyUpL|}o({*j--j3Sx7jp0AmQM1Y8n>C4NJteI_@HT> zd7XRSd?q`~g3llK26eA1dBgEz$xhb|>3{o#<Fa~J8c#p=Zt?D;4;o7i&0E{$d==*} zd-iRL{Ho68V`cq!+qi$0{7s9&KZ65~_?$aTpmu}(|I&1>A4^)!AOHR;kfk;*^4g-+ zQ{HRZ9*ee||Kru9io{csZd9ghzmc~(@8X?naV<4+mCICTMO%g0)O?TIc=<x!(p(v0 zs;hfyVCw?^mp*S=RCNAg^?I2PJHPeC+*F_X#nW<5UtjD}{_OgDGm5NjtF9gMJG|@Y zy4xl$+0i13^DVifAN!x*vdqg)aO+k<`C9+?H}X7cr~XU24Nd_B>b@z?4hNRYbY*Wm zR$2a}!M|E&bNTs@<I8R1B4-3yy!><Pw)m3W!3We;Yd(FQ<!8J5+P6K5rT1UmT&!1i zV^{Q^YM<{J(YNH*dV4F*SN(Wla`)n$rfcVHaz4Iqy+6-=pA+f2aWgduXNAUICx-*e zz1Bb6Bl6^A<J`Um|0NCQCn;Soe*3n#`;_hSKX21EsywX;GFxu3ZRv;elSD3g-raU- z|F=go_Mg8#ZOP}7j(er1hP#*Aruk)eUSGCtiCAp!$9Zpq4lJKB-^uQ@*3UC>7iKxx zo!z)Yb&=iO&z-fuk4^jgdexWdw~1QhR?EUtbIm<p_TAr$xu1jgs6U$V{j1OZVAlDp zxvmeM---@Cp#FD`Ro;h94d=TO{Q17<@P9Ai`u*f}vs8BSbjDKG2hXn^7eAJ)Z+3jQ zyZJJ)*t^}!wcfoBIjP=WBUh7DsN*-a=2h^v=Tn-09X}AaGJm=wp>ZY-0fh~L+#Ekb zUVi;!z*-a3r`}gt&-cT{Cw79?k0pg?b=Qj@zuYGo6!QM%iW{3375P8cvXgC`-=}4N z``gOigLkc&tghP`*sCq!vitq^lJPd_TNAcx9$PiFCQiS#CP;ODTi(`>H~GJG+u63% zOe?e`A|07|F*45A+B@z4p3*qM50+)~<5sGMuX?cAAoPRh@$9^rp)vM<TNa*Xw(~CL zUN_~ECTmqufO>n`tsv3))4m){TUm8}gWuKiy0YVQX4q}lwc8c`UgFC7eLuRxxPF$b zdh+?#pKBElKDxT9&OerC`cc1~zoYHfEG^9&L`7dLD@)C_&^Nm0;x!*kzNVq|V~NN) zQ}gpa59fZfcsx0Jd0C9?)ygoL;0x+!&mNq2Y}c$8_XR$_TJ$Ys=W?|wUGAqko|FGS zHs9P;sV;xV{?(;DL7elaI@xKeT)8E7an7^zUW)VoSLXzpEcde8TXp5Aef@^|lGES8 zCl+D!n9ycGK`m$hsc|QFZhyjM_qOoOzP;U#uLQBqzkmM3^v821x0iGMC<*zX$y!_F zI)BPp%_h5_h3sb6T>3>$tn|OTHEz~TDe?QJ>Qd5K$!EfNH?CM}J4;Nz_t~{?Un(<o z{GNLkTP~O-^Cz>v&18N4`CBVIzn`nI-OA~G{qs+8JChAwLfb%xA`vJb1R}&48RwT? zjOyR{{GQGF6<3O51V41n*rd>!eQE0Idh@*;_iDZyD|c(zSiadDv_XCD^BTXvlj^lI z?Wga1KXX^1?Uyf$DpH(+xn7B!o?2w{x~A@#Ve_@e>x;DsS*+mD;INd1rAAA2_o-d> zKmOiXRDA8D=7Y)6MZU2Yrg6Pm(r`XM-q-l{WA^&(XFoOg^L>7@;Cz>X|EdR@XDvAY z{@r}(+Un~^8|=Pb-0PleVs>qrmaS>v?5GM|PwuTtTR-hJzhkiU#`neS`I76;XLcXu zo__4Kadgnu`Cn3hJzJB^zW&n@KVnL5Gd6I_Z&i9Qd6UWU_w%DmTz&LHKkUrR5`1>E zYW~5C%lDazK3b{SWO3H}b&{L+_Ae!}&zHW=|GMKtqlk*vZ@J$S*M3%P`?^m5+s;4l zeqNjuRr&XK;OtP(LqxS9UMnd)*sK@z+VK1D{obNAL8fzszr7D;$zC=^>&G1n)wQ?e zp39hOPknQeaehdd`t<iJJrB++$>`oSL;COIpG%b&t=#)Nx~}PV{;ZHY)o<L}#m^h= z@!$T)a@~&f#KJ$H+JEhTy7eypxn)Q^23RsO&Odc?-Ih<^>#Ubs=(E<SEpPDm`w&xl z_;ldxeLugstBc)Vzs|}3irJAna>>bsf74!Wn-z0o`HB;pw;h*{{Z%9Q#PVIfyBOpA zM9l}2#eZD2<?broSL0I2Waqn5>cSb;wMXw?y1IUsNX)H8@!MCdKU?1rC^W-zzv--~ z;^J*vr4O^2upQdE*@(a>5DUkiyR0lVtG=-PS7xs%Vx7<YOYFGJhfII_@V{w6SC)C% z-naa;RBwLD(gXg!xjJj#PkmO}l6<S!WJUD*6`3#Ow|=c%r1v-a?vG3L_LH~wKkkzW zeKTA8#}n@9;*9gZ{W#UmzUJMv(p`10Cl^1O5?|UfHLN^&`>t6x@(R9J{NGmlXiM|9 zo;Y61M_;bn_Wk_%x$r;t=j&1rUmBP7-s>&Rw!QB{uqXWBtv0A#TmD_%{3(~+X(qdu z7M1@tHJtw{+<m}zrHa4ih2WK+ANSocdn<0Z$G`TY&Gh?owSFwozRL4s3Gej&wd^KG zDj&NYkyEbL-~a9UkB1HNcCzQs$LzUQzpLSVRPaoKv3>6wxUJH9e{W&g#>?t_Z=$!H z(9-(xL@f6D{(z8cFEh4Yl`~!08oF)^*de#p-KclDpLg`*r`^`8j?MWu@l*JRpSKdD zkKVhzM&{1UyIWM$_Fa?NUs+Q2C-c*xLrd!WckTGiZGH6F-r^T8))&92xb}GCf}Ohi zW@t&h-F^3!w&B#0vfHcDue`h%`lx{5rr6owA&|Iyz8_B>oKydw_O!v@eZg{(H!JUL zEIy!ae|>-LmOqU1yZhut+xy<$Us`bQb5r#)@Tk;}e;u)GM_P~fy(xOPtZw7V)Xbdv zXURwBR~|jNIC=iXe7O%dulW9{CD^(-z{SWoUsO!=@0YpKaq(gwUQYj(*+2cczGhVF z)TpAXBH_gk=Y<?p{~PkF#^vH7?MIt-KNFu?v+vh-=F^|P-~aVwj>Xe(;qI;1L_VAp zE;{l2#C$vH{yldBPd-0;%kSyWwYhIE2Fx$LcPnDk7TZ^+1AeO6yKmpM{h!R-`E{4p zJiGDy&hjqr|G$6ACZCz`ZtnhXGr_G}d^P$HZE=AQD=*bE-2Zp>^-;h1HX5uoK|xQS z{>(kyWV6)4zqEg6lYd<wujGf53if`XHT$Zg-9_h1|4a1QlU=cX>b(<3MCx|F*e6+d z|MTbMS9hjmL|&3}zO=dj)*3^<JH-Jt8PnHkvrH8`**I_h-P!Nz6YFdYw`z(l|6MMB z&G_4`W&Sc-e;>H9tKK>0--YbLtIC^`HphJ}JlT1BT3+qpkA3OSj`jAR?Ng7B6w%+e z&4TUAn*O=l-UwFry^@`OxBNsj-ya)`LPwd>i^WIl`s-__C*3vAdB+4PqwrM6AHu<z zDttb(-CNCo;zy2_6E4Z=KW*^0I<LE{=z?19idU5v7irrbPyKJb{>Hy-_ptp<{>%3N z)GvPYe*KFTKZ@TSZ##bc{??nH%!_U|uRnS{f0m5q;?1`v-j0m@wK8}2np2-|HK~5Q zsjYsDS<~V2)f-Bzt?O)4ckg(2e*N*va^LQne;;02?@!F)6P;Uo`pS+U|I_w8+<mDv zw(VPw{Bp5m%Ze@Q9xpqde%wCJbR`GJ-imM5*S?m0=EN6~pvFd{Zw#nqt-quAfco|a zT3SDJxPLrZaQ^r9bve<?elIuE=NEYT*KL`BcU>Q^?1z(z^VZ+9*jd57v+7*q?eDsF z_y1kF^W)1?eX;q)AD*2k=ku9bUUl@rm8dsQuSY-H6jG`lzVGDYE6m$E^y?E&KPrA1 z&c`bIC*J>f%ggR=Z)^Uyg6{j06W+|wdUwX_;QTvcI^pkb+JwHXbdNIME*2eoK%IR; zk$`6Z_nsG`$M>awmN4E??KAtj)Xr7sude*%FW3C5r2F^29l>96R_LC){w;i6z?ZYL zmmj-O$NTX?__1SOw!C|Px$eAAyvX_UIeog9@9#eyX8ZWV+~3DHF!E;aUQ<apYpN-M zic;~)b6;geYwW%@{&U^%Y-y$UB}377p7uKz`7dR*`|caL=Te>WuWQe9A72VNs6PF} z&s)dCLe53XPm-Bp3CfzPdRBEIGFD}0b+`EKez$bTH-Fi;r{#W~I=3-({}n5pUg5f* zPIo@v+4ExRAtUp;vfnrAPMl&~A@jiU+4J*9|L@(QP-TAYSn&NDy5f#ecAsWdzP0JQ zQR?vL!-|I&BlZ`4c=phfJE$Wg=}cZ!(3#g-4=(Slz2;fX+huIOrSwYKE%o~ms}4-I zum89}_i;Sm9i{r1*B{r0$i7&<viFAdws(^MKbc31o&WpmP+$71Jsx~~wSPCPeI)$; z?w-%7^NCJAKYAJGUs<I6_la1n>*{qPUsj&_xvADoclCqfmvg1{9!y^SqUO!=^J2O? zw%y%hcx?W*%b&Izruup7_-f9Z$6LCw^1X=LtE4BVEaP|md-`bh|L^OQr-yI%tXz8I zH@p9_DK~X5u6K{M`S5h3CQHcmqg;afeR8+&`*>UPXn8DaP2Y|JCmXA!nr{5B<hIY< z<!o|g{iJu1+g{yY8gN-x^Uf4z&;CER&#k_@es#OW*H@l}hm$vVi^Y0hx<6-ry$_ek zTgz4DCH@PSfM%P%@3+{$W8VA!L0p2hb+XPbv2R}!oV-@}@C`KVe5v}^we{AwEauM8 z`tfARu_ZrOI(c&yl}wp?QvTA4M?X)biq`1uEY92b_4?|NgX-<!%g*QPuKE^ZP#0yg zRYzCrQOx)9<gRbwM-Q)gy1sh;eEH*lAN@@FW;*|--nU!J%0F6e-%<6w{C`l*zdLo# zkM8qH?z$x!efw7T&Yel;Rx!?JJ)67nab&;v_qwMK7Jl^q%^&&JYUk2x_DjOzD*t>d zzAb*WBFwsSs=nPnd)cWSpPt@ndcDfOzwaTlboi{&|J<J+U&)T^nt5_vbgb{4d57E< z2hTRZen2{Mmbvi-+)(Q4Zt&;YKBuBU^TA}TA4|4)uWUNM@35BiwH@VNpS^zfo?5%% z(m&S&>bBRf_iNR6uVb`*y|{Y&RPB3bGPAl}*LqK1{d`;0|5;j>=2RV+bvtV7(o<%Z z5+?b9(PyIXe0<-(lIa5L<K^PnrF=hZrbOk}zDYkXdR^b5Dy1f6JLBo^uU{SB(LJx5 zT}`jT`M~qe@L7+V#eFpLeqL+e-gmeC>#JAa9^G0JdG_PZ)oP;iucw_q$6fp3xNy>= z?C#*Di}DJOo?JXVT<MjKdi}o7Gj|qVTB<KLo&WyM`ae&01g`cg^lKng9p*HF8#^o0 z<ELx?*m8V%&;j-EjWbR3LqD8Uejb0Raw)T2{fs?3ZmipKe?`ba^<3%c_YBQtZ$G}@ ze{0X<-(UaKW@iPLdS!)gHB;XH^w+=RZfmv`o!2goezgC;fwG*>!`b3N49ikI>mIFh zf8KE3w0@3g{GU?y*Xtnl>5o^<mX%v=%<pf`Vx52A&aS#LWBKdY-TR_Wibb_AH@NUP zcy^M@st?8Yr-yAZdHu_ih><NbR&amis3_xnMpxCjo}YtQ=YI)a^&oiiB=f5__tio_ zoD^-bVzQgaWcPIa1LM5!jT`??kkjV8e$B1N=iwCDZ7&3)zvsVr5wPu2Pv7J_pI*&h zKXXgaisj+XxjZjI<^NV+alQYp^k{ki5-q#R622dIrZidp-?3)axvMKnuirZzRbIO0 z&$0XeOTI4I@p0qLe2yP6zn5QsyybPAR^ExVn|GCa^Y0gX{GvQptbYIb2U}<Cd>6d0 z<nQUiugaUdwxtQ5yOX<ie&*fsB6fmR`i6}hpb-+^Ikj)IH4g~8vd-W0^hglve66?f zIg{lT!g;)u!#|vSJSpa&I&1Ad!*zc|Yaj)=*;bY4<A$Yv-ZCZkE^k~=nJM_}=cj|t zj~~eXK00CF%C!;83*UeK{CG#aO?=EtnGYvL^NN<vwy7(4^1<TQ-E2O6*}{h{6(wGm zrJ3ybmxsl>>t%l}J=uBt7^p_Qc4xx=bNec@{=C^Ic~sfv+t%2m1TGuN4Q9_bGB9vo z@^o<wLC%7pDjdV32bO^bqnCc)t@OZH;q8q$mYSl~>lQWnTivgZ_}*oA(sre5`18Np zG+Ar%-e+9gYRFn+6@KpLPtMy%4qAVGeMv3+?#Zj=pI*p(n7Mm}ZkgThkB@!@zL$}( zmoZ=T<IdIf*Tr-?_wN6nqLUT-tF6KR-@Cc@>lW>J`^CBKnEgBRC974hy^gx~*uC6W z`<g{g&i;z~+iI`vxOZ~;K8bQ0V{^0jvGVo*Ug>4edF@TGE0b^;QndbgW-2RM<7c&` zKJaAvqdTgy*3Y}FG+%7~|KSu{&9VJ^Dwh7;_A)%4rA}{Wvfsx4=63r&hJ5(>E_eNr z{`WKEY~IgWb*1L(v6F>2o7anewqmdO)Y`xFlr{fSE}O4gO$*OwS|9sxMQqLLk7w`u z_jbQ~zoKKa>nedSA6{CoH+gG&|F1Ww(P6!8TGIAEm%jy0zxFyyCo{X>QZr(Td!GEY zfcszfguc&>zBTWVhyRi+*Id`Fi(|fKlqUasw5{U*PXD8?xq~${x5}P=dz^;|AA=j$ z)^UO#UaolXIVtO6=&BW$KPo=WnHKtG<*}b;e|Ig739ODOist&UWTPQ#P2JZE*@eG& zCwD)Oip^dlbI<Zu{k(ovO@+zFS250SkB^dY|M%tQqum#qcO5@(ztJ~m^Utmc{@eC_ z;J^I%cljPE%aqCP;f(WF%gq%1U-kF&(eLs0IjevCu4NOSZ~Z^Z_u;(BL8)`A=gk)H z{krwzf$7J^M9ZwsM_zs&6gp2vZr76|xk>xq?#g5Rwsp4J@umDFEVK5_kLs8GthV;} zwJpov!zMCtCfUaA{oonstC1_4{Q2I^DS6SLw0|<QopjL?F1wvY7mjX!4jTA6cCq=? zCyVWNmg;^#uO3NS8`Jyj!p6VXEAKu}*X!e-^W8`A-pREq?50O0%WiSrFEMe|2cLO& zWp=h+_y1?0+_t34T7Un&m^J&e4yecf`D3bo^lSgVxSIQ`7n<F(URB<6ca!@WpWKcA zOezy?tZNHpur4hU6Mg+8;4G+JW!Su1{PiEpwZ%sdZ|*AapZ5W_uxQ5aZTsiVo;dIE zA<m5(QZt`C|MRCY`bcLrU)Q{s#d5*Vw;b7?H&beV^&;h-rB&x|otlO9o&x0TY{m;N zi;r(!cW!EU(Sh`YK+ESxuPoBO-m7U^WVPUs?yh%!y1T62Z#QJG`K0~xX07p~H?x;N z>p$&Y8|(WdETU4zU|Ig0x)T?aY9^oG&GqAnmhJQY`C`xS{mjzQzBGBcp1l0cT!%U4 zc`ktq)!W;4as7C5($eM2O>Om~kAr7VyFYvN+bLTAr|3QVS~um|>D+_YN}qf<w= zDsJKmHC@9k@y;eoFPwj~Of+#rOz(?J{<)x0mNl=%o`DZNNA?LuO2`iedu@mD`gxHd zXDnZCxUxu_)j#fpXpL9r>=K&-7f?wRtNGw^<^J5?*Ttu{=<UocE42AMqj!0eKj))q zA-pfFx9<}YPA?64dGdPuaxFXdw-?NdcCR_PIXY?o|C&`Z?u8y$zU5r-dc7`oHNB4g zdoGE5o<04ToO0FCc{^vFzcA1B>%7bBs#mVy+On&`KTe&!yKX|0lew*(^|B>hR+cs) zQoHl6ZqB&-bz!X4D#oen*1t9k|MWAB%e1$??4ESq*~Ue8<lb!Ae{)d=^S3rmB34_> zUIR+%ac{dC&L0rjlKEcbgXhwyq9_&dT1_VVtCr^(=kK|Hbo*J)+uPlDGTBXifVx;n z>${wK{I8@tAJ5!5akUV%P$+-bpFc-)4Ha7ptCovJMKAZV+AIC~-j{i$Gbc2A@ASRN z{W3nT`nTfU<TDfGTz{`CjXWMhMDYVDaBV)Xn&~>>*^|RX-RnXQs&9R9YeI6#{*Z&} zwg--GU&dwkvLbAz){mSM`&WLx=kFgqB`SKWsAyHu)_GTZ@1(und%xOf+4=0=J4vsb z7Q3&@TlE@e0RT@Ujl7_)mg={sjPv=IHTZj73E7!&KI^+Slikb8b5`4Iba$;f@^jhk zIreo`HSfQLcU6kZ+H3L2-rj$@`rOXctUpigNggeKUQ-cKQ*{39?m5qu;3nf_BshcT zKL7P-r;AK2Dq@}AoB8L{w3{yrZv`Jn|8VR}#OuF0mxEmA@z42QawhVM7^to|vhHv4 z_r$%AWM9VTd75R0dB=QvaHitN($=GKJ5QYtoi8U_nz@dM`I#FtSU`h}X6JTnTI?F` zd-l^UTh|LI$(Hx8FxmawYWMTiB8z|G=Z~*x&CYAzcKpP>kCT(#k1epfyXs4PGcj|a z+4>3(Hh&9yZTeX{yktvb)OuH~iqGc#x3_5hn6g4_n@(@`e+%t(^(AFK^X|s9)b6@^ z^~eXtUc;=-$9}kEoh&^tW8c!3eMH1sub;z#^u^Vxaw{e=t<8PwdV1ZW2|=pno6K;A zI=n@_z=w&+F12Y34`HPZ42@o(#+uXBS%lrh$i#9AH1DkDaasUt`v|Q`zoMm~!Cy6G uttZ@Nco?IOMh%?NkVdK5NKGDp>gRM;X@+kXv0`9gVDNPHb6Mw<&;$T6%~Xj1 literal 0 HcmV?d00001 diff --git a/career/img/file.txt b/career/img/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/career/img/numoc-16-9-blanc.png b/career/img/numoc-16-9-blanc.png new file mode 100644 index 0000000000000000000000000000000000000000..e877fb5f7e5d52c439bdcf5882a2f1ce4c429d93 GIT binary patch literal 86910 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%0Axidp8oL(2N+Od)S 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ᣀVJE+h@*cuD2v?zVot^f8U85QCogZ>9<wI@h3ZS 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 literal 0 HcmV?d00001 diff --git a/career/img/rose-rose.jpg b/career/img/rose-rose.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77b23e9704a58b1ec1c5399d56357d600c9b3546 GIT binary patch literal 45517 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 literal 0 HcmV?d00001 diff --git a/career/index.php b/career/index.php new file mode 100644 index 0000000..87fb3fa --- /dev/null +++ b/career/index.php @@ -0,0 +1,12 @@ +<?php + header('HTTP/1.0 403 Forbidden'); + + require_once('../../config.php'); + + $PAGE->set_url($_SERVER['PHP_SELF']); + $PAGE->set_pagelayout('admin'); + $PAGE->set_context(context_system::instance()); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('error')); + echo '<center>' . get_string('nopermissiontoshow', 'core_error') . '</center>'; +?> \ No newline at end of file diff --git a/career/js/file.js b/career/js/file.js new file mode 100644 index 0000000..767b3fe --- /dev/null +++ b/career/js/file.js @@ -0,0 +1,48 @@ +(function () { + $('#btnRight').click(function (e) { + var selectedOpts = $('#lstBox1 option:selected'); + if (selectedOpts.length == 0) { + alert("Nothing to move."); + e.preventDefault(); + } + $('#lstBox2').append($(selectedOpts).clone()); + // $(selectedOpts).remove(); + e.preventDefault(); + }); + $('#btnAllRight').click(function (e) { + var selectedOpts = $('#lstBox1 option'); + if (selectedOpts.length == 0) { + alert("Nothing to move."); + e.preventDefault(); + } + $('#lstBox2').append($(selectedOpts).clone()); + // $(selectedOpts).remove(); + e.preventDefault(); + }); + $('#btnLeft').click(function (e) { + var selectedOpts = $('#lstBox2 option:selected'); + if (selectedOpts.length == 0) { + alert("Nothing to move."); + e.preventDefault(); + } + // $('#lstBox1').append($(selectedOpts).clone()); + $(selectedOpts).remove(); + e.preventDefault(); + }); + $('#btnAllLeft').click(function (e) { + var selectedOpts = $('#lstBox2 option'); + if (selectedOpts.length == 0) { + alert("Nothing to move."); + e.preventDefault(); + } + // $('#lstBox1').append($(selectedOpts).clone()); + $(selectedOpts).remove(); + e.preventDefault(); + }); + $('.wrapper').find('a[href="#"]').on('click', function (e) { + e.preventDefault(); + this.expand = !this.expand; + $(this).text(this.expand?"Réduire":"Voir la description compléte"); + $(this).closest('.wrapper').find('.small, .big').toggleClass('small big'); + }); +}(jQuery)); \ No newline at end of file diff --git a/career/js/interact.min.js b/career/js/interact.min.js new file mode 100644 index 0000000..4fdef50 --- /dev/null +++ b/career/js/interact.min.js @@ -0,0 +1,5 @@ +/* interact.js v1.3.3 | https://raw.github.com/taye/interact.js/master/LICENSE */ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.interact=t()}}(function(){return function t(e,n,r){function i(s,a){if(!n[s]){if(!e[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(o)return o(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var p=n[s]={exports:{}};e[s][0].call(p.exports,function(t){var n=e[s][1][t];return i(n||t)},p,p.exports,t,e,n,r)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;s<r.length;s++)i(r[s]);return i}({1:[function(t,e,n){"use strict";"undefined"==typeof window?e.exports=function(e){return t("./src/utils/window").init(e),t("./src/index")}:e.exports=t("./src/index")},{"./src/index":19,"./src/utils/window":52}],2:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n<e.length;n++){var r;r=e[n];var i=r;if(t.immediatePropagationStopped)break;i(t)}}var o=t("./utils/extend.js"),s=function(){function t(e){r(this,t),this.options=o({},e||{})}return t.prototype.fire=function(t){var e=void 0,n="on"+t.type,r=this.global;(e=this[t.type])&&i(t,e),this[n]&&this[n](t),!t.propagationStopped&&r&&(e=r[t.type])&&i(t,e)},t.prototype.on=function(t,e){this[t]?this[t].push(e):this[t]=[e]},t.prototype.off=function(t,e){var n=this[t],r=n?n.indexOf(e):-1;-1!==r&&n.splice(r,1),(n&&0===n.length||!e)&&(this[t]=void 0)},t}();e.exports=s},{"./utils/extend.js":41}],3:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=t("./utils/extend"),o=t("./utils/getOriginXY"),s=t("./defaultOptions"),a=t("./utils/Signals").new(),c=function(){function t(e,n,c,l,p,u){var d=arguments.length>6&&void 0!==arguments[6]&&arguments[6];r(this,t);var f=e.target,v=(f&&f.options||s).deltaSource,g=o(f,p,c),h="start"===l,m="end"===l,y=h?e.startCoords:e.curCoords,x=e.prevEvent;p=p||e.element;var b=i({},y.page),w=i({},y.client);b.x-=g.x,b.y-=g.y,w.x-=g.x,w.y-=g.y,this.ctrlKey=n.ctrlKey,this.altKey=n.altKey,this.shiftKey=n.shiftKey,this.metaKey=n.metaKey,this.button=n.button,this.buttons=n.buttons,this.target=p,this.currentTarget=p,this.relatedTarget=u||null,this.preEnd=d,this.type=c+(l||""),this.interaction=e,this.interactable=f,this.t0=h?e.downTimes[e.downTimes.length-1]:x.t0;var E={interaction:e,event:n,action:c,phase:l,element:p,related:u,page:b,client:w,coords:y,starting:h,ending:m,deltaSource:v,iEvent:this};a.fire("set-xy",E),m?(this.pageX=x.pageX,this.pageY=x.pageY,this.clientX=x.clientX,this.clientY=x.clientY):(this.pageX=b.x,this.pageY=b.y,this.clientX=w.x,this.clientY=w.y),this.x0=e.startCoords.page.x-g.x,this.y0=e.startCoords.page.y-g.y,this.clientX0=e.startCoords.client.x-g.x,this.clientY0=e.startCoords.client.y-g.y,a.fire("set-delta",E),this.timeStamp=y.timeStamp,this.dt=e.pointerDelta.timeStamp,this.duration=this.timeStamp-this.t0,this.speed=e.pointerDelta[v].speed,this.velocityX=e.pointerDelta[v].vx,this.velocityY=e.pointerDelta[v].vy,this.swipe=m||"inertiastart"===l?this.getSwipe():null,a.fire("new",E)}return t.prototype.getSwipe=function(){var t=this.interaction;if(t.prevEvent.speed<600||this.timeStamp-t.prevEvent.timeStamp>150)return null;var e=180*Math.atan2(t.prevEvent.velocityY,t.prevEvent.velocityX)/Math.PI;e<0&&(e+=360);var n=112.5<=e&&e<247.5,r=202.5<=e&&e<337.5,i=!n&&(292.5<=e||e<67.5);return{up:r,down:!r&&22.5<=e&&e<157.5,left:n,right:i,angle:e,speed:t.prevEvent.speed,velocity:{x:t.prevEvent.velocityX,y:t.prevEvent.velocityY}}},t.prototype.preventDefault=function(){},t.prototype.stopImmediatePropagation=function(){this.immediatePropagationStopped=this.propagationStopped=!0},t.prototype.stopPropagation=function(){this.propagationStopped=!0},t}();a.on("set-delta",function(t){var e=t.iEvent,n=t.interaction,r=t.starting,i=t.deltaSource,o=r?e:n.prevEvent;"client"===i?(e.dx=e.clientX-o.clientX,e.dy=e.clientY-o.clientY):(e.dx=e.pageX-o.pageX,e.dy=e.pageY-o.pageY)}),c.signals=a,e.exports=c},{"./defaultOptions":18,"./utils/Signals":34,"./utils/extend":41,"./utils/getOriginXY":42}],4:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=t("./utils/clone"),o=t("./utils/is"),s=t("./utils/events"),a=t("./utils/extend"),c=t("./actions/base"),l=t("./scope"),p=t("./Eventable"),u=t("./defaultOptions"),d=t("./utils/Signals").new(),f=t("./utils/domUtils"),v=f.getElementRect,g=f.nodeContains,h=f.trySelector,m=f.matchesSelector,y=t("./utils/window"),x=y.getWindow,b=t("./utils/arr"),w=b.contains,E=t("./utils/browser"),T=E.wheelEvent;l.interactables=[];var S=function(){function t(e,n){r(this,t),n=n||{},this.target=e,this.events=new p,this._context=n.context||l.document,this._win=x(h(e)?this._context:e),this._doc=this._win.document,d.fire("new",{target:e,options:n,interactable:this,win:this._win}),l.addDocument(this._doc,this._win),l.interactables.push(this),this.set(n)}return t.prototype.setOnEvents=function(t,e){var n="on"+t;return o.function(e.onstart)&&(this.events[n+"start"]=e.onstart),o.function(e.onmove)&&(this.events[n+"move"]=e.onmove),o.function(e.onend)&&(this.events[n+"end"]=e.onend),o.function(e.oninertiastart)&&(this.events[n+"inertiastart"]=e.oninertiastart),this},t.prototype.setPerAction=function(t,e){for(var n in e)n in u[t]&&(o.object(e[n])?(this.options[t][n]=i(this.options[t][n]||{}),a(this.options[t][n],e[n]),o.object(u.perAction[n])&&"enabled"in u.perAction[n]&&(this.options[t][n].enabled=!1!==e[n].enabled)):o.bool(e[n])&&o.object(u.perAction[n])?this.options[t][n].enabled=e[n]:void 0!==e[n]&&(this.options[t][n]=e[n]))},t.prototype.getRect=function(t){return t=t||this.target,o.string(this.target)&&!o.element(t)&&(t=this._context.querySelector(this.target)),v(t)},t.prototype.rectChecker=function(t){return o.function(t)?(this.getRect=t,this):null===t?(delete this.options.getRect,this):this.getRect},t.prototype._backCompatOption=function(t,e){if(h(e)||o.object(e)){this.options[t]=e;for(var n=0;n<c.names.length;n++){var r;r=c.names[n];var i=r;this.options[i][t]=e}return this}return this.options[t]},t.prototype.origin=function(t){return this._backCompatOption("origin",t)},t.prototype.deltaSource=function(t){return"page"===t||"client"===t?(this.options.deltaSource=t,this):this.options.deltaSource},t.prototype.context=function(){return this._context},t.prototype.inContext=function(t){return this._context===t.ownerDocument||g(this._context,t)},t.prototype.fire=function(t){return this.events.fire(t),this},t.prototype._onOffMultiple=function(t,e,n,r){if(o.string(e)&&-1!==e.search(" ")&&(e=e.trim().split(/ +/)),o.array(e)){for(var i=0;i<e.length;i++){var s;s=e[i];var a=s;this[t](a,n,r)}return!0}if(o.object(e)){for(var c in e)this[t](c,e[c],n);return!0}},t.prototype.on=function(e,n,r){return this._onOffMultiple("on",e,n,r)?this:("wheel"===e&&(e=T),w(t.eventTypes,e)?this.events.on(e,n):o.string(this.target)?s.addDelegate(this.target,this._context,e,n,r):s.add(this.target,e,n,r),this)},t.prototype.off=function(e,n,r){return this._onOffMultiple("off",e,n,r)?this:("wheel"===e&&(e=T),w(t.eventTypes,e)?this.events.off(e,n):o.string(this.target)?s.removeDelegate(this.target,this._context,e,n,r):s.remove(this.target,e,n,r),this)},t.prototype.set=function(e){o.object(e)||(e={}),this.options=i(u.base);var n=i(u.perAction);for(var r in c.methodDict){var s=c.methodDict[r];this.options[r]=i(u[r]),this.setPerAction(r,n),this[s](e[r])}for(var a=0;a<t.settingsMethods.length;a++){var l;l=t.settingsMethods[a];var p=l;this.options[p]=u.base[p],p in e&&this[p](e[p])}return d.fire("set",{options:e,interactable:this}),this},t.prototype.unset=function(){if(s.remove(this.target,"all"),o.string(this.target))for(var t in s.delegatedEvents){var e=s.delegatedEvents[t];e.selectors[0]===this.target&&e.contexts[0]===this._context&&(e.selectors.splice(0,1),e.contexts.splice(0,1),e.listeners.splice(0,1),e.selectors.length||(e[t]=null)),s.remove(this._context,t,s.delegateListener),s.remove(this._context,t,s.delegateUseCapture,!0)}else s.remove(this,"all");d.fire("unset",{interactable:this}),l.interactables.splice(l.interactables.indexOf(this),1);for(var n=0;n<(l.interactions||[]).length;n++){var r;r=(l.interactions||[])[n];var i=r;i.target===this&&i.interacting()&&!i._ending&&i.stop()}return l.interact},t}();l.interactables.indexOfElement=function(t,e){e=e||l.document;for(var n=0;n<this.length;n++){var r=this[n];if(r.target===t&&r._context===e)return n}return-1},l.interactables.get=function(t,e,n){var r=this[this.indexOfElement(t,e&&e.context)];return r&&(o.string(t)||n||r.inContext(t))?r:null},l.interactables.forEachMatch=function(t,e){for(var n=0;n<this.length;n++){var r;r=this[n];var i=r,s=void 0;if((o.string(i.target)?o.element(t)&&m(t,i.target):t===i.target)&&i.inContext(t)&&(s=e(i)),void 0!==s)return s}},S.eventTypes=l.eventTypes=[],S.signals=d,S.settingsMethods=["deltaSource","origin","preventDefault","rectChecker"],e.exports=S},{"./Eventable":2,"./actions/base":6,"./defaultOptions":18,"./scope":33,"./utils/Signals":34,"./utils/arr":35,"./utils/browser":36,"./utils/clone":37,"./utils/domUtils":39,"./utils/events":40,"./utils/extend":41,"./utils/is":46,"./utils/window":52}],5:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t){return function(e){var n=c.getPointerType(e),r=c.getEventTargets(e),i=r[0],o=r[1],s=[];if(p.supportsTouch&&/touch/.test(e.type)){h=(new Date).getTime();for(var l=0;l<e.changedTouches.length;l++){var u;u=e.changedTouches[l];var f=u,v=f,g=d.search(v,e.type,i);s.push([v,g||new m({pointerType:n})])}}else{var y=!1;if(!p.supportsPointerEvent&&/mouse/.test(e.type)){for(var x=0;x<a.interactions.length&&!y;x++)y="mouse"!==a.interactions[x].pointerType&&a.interactions[x].pointerIsDown;y=y||(new Date).getTime()-h<500||0===e.timeStamp}if(!y){var b=d.search(e,e.type,i);b||(b=new m({pointerType:n})),s.push([e,b])}}for(var w=0;w<s.length;w++){var E=s[w],T=E[0],S=E[1];S._updateEventTargets(i,o),S[t](T,e,i,o)}}}function o(t){for(var e=0;e<a.interactions.length;e++){var n;n=a.interactions[e];var r=n;r.end(t),f.fire("endall",{event:t,interaction:r})}}function s(t,e){var n=t.doc,r=0===e.indexOf("add")?l.add:l.remove;for(var i in a.delegatedEvents)r(n,i,l.delegateListener),r(n,i,l.delegateUseCapture,!0);for(var o in b)r(n,o,b[o])}var a=t("./scope"),c=t("./utils"),l=t("./utils/events"),p=t("./utils/browser"),u=t("./utils/domObjects"),d=t("./utils/interactionFinder"),f=t("./utils/Signals").new(),v={},g=["pointerDown","pointerMove","pointerUp","updatePointer","removePointer"],h=0;a.interactions=[];for(var m=function(){function t(e){var n=e.pointerType;r(this,t),this.target=null,this.element=null,this.prepared={name:null,axis:null,edges:null},this.pointers=[],this.pointerIds=[],this.downTargets=[],this.downTimes=[],this.prevCoords={page:{x:0,y:0},client:{x:0,y:0},timeStamp:0},this.curCoords={page:{x:0,y:0},client:{x:0,y:0},timeStamp:0},this.startCoords={page:{x:0,y:0},client:{x:0,y:0},timeStamp:0},this.pointerDelta={page:{x:0,y:0,vx:0,vy:0,speed:0},client:{x:0,y:0,vx:0,vy:0,speed:0},timeStamp:0},this.downEvent=null,this.downPointer={},this._eventTarget=null,this._curEventTarget=null,this.prevEvent=null,this.pointerIsDown=!1,this.pointerWasMoved=!1,this._interacting=!1,this._ending=!1,this.pointerType=n,f.fire("new",this),a.interactions.push(this)}return t.prototype.pointerDown=function(t,e,n){var r=this.updatePointer(t,e,!0);f.fire("down",{pointer:t,event:e,eventTarget:n,pointerIndex:r,interaction:this})},t.prototype.start=function(t,e,n){this.interacting()||!this.pointerIsDown||this.pointerIds.length<("gesture"===t.name?2:1)||(-1===a.interactions.indexOf(this)&&a.interactions.push(this),c.copyAction(this.prepared,t),this.target=e,this.element=n,f.fire("action-start",{interaction:this,event:this.downEvent}))},t.prototype.pointerMove=function(e,n,r){this.simulation||(this.updatePointer(e),c.setCoords(this.curCoords,this.pointers));var i=this.curCoords.page.x===this.prevCoords.page.x&&this.curCoords.page.y===this.prevCoords.page.y&&this.curCoords.client.x===this.prevCoords.client.x&&this.curCoords.client.y===this.prevCoords.client.y,o=void 0,s=void 0;this.pointerIsDown&&!this.pointerWasMoved&&(o=this.curCoords.client.x-this.startCoords.client.x,s=this.curCoords.client.y-this.startCoords.client.y,this.pointerWasMoved=c.hypot(o,s)>t.pointerMoveTolerance);var a={pointer:e,pointerIndex:this.getPointerIndex(e),event:n,eventTarget:r,dx:o,dy:s,duplicate:i,interaction:this,interactingBeforeMove:this.interacting()};i||c.setCoordDeltas(this.pointerDelta,this.prevCoords,this.curCoords),f.fire("move",a),i||(this.interacting()&&this.doMove(a),this.pointerWasMoved&&c.copyCoords(this.prevCoords,this.curCoords))},t.prototype.doMove=function(t){t=c.extend({pointer:this.pointers[0],event:this.prevEvent,eventTarget:this._eventTarget,interaction:this},t||{}),f.fire("before-action-move",t),this._dontFireMove||f.fire("action-move",t),this._dontFireMove=!1},t.prototype.pointerUp=function(t,e,n,r){var i=this.getPointerIndex(t);f.fire(/cancel$/i.test(e.type)?"cancel":"up",{pointer:t,pointerIndex:i,event:e,eventTarget:n,curEventTarget:r,interaction:this}),this.simulation||this.end(e),this.pointerIsDown=!1,this.removePointer(t,e)},t.prototype.end=function(t){this._ending=!0,t=t||this.prevEvent,this.interacting()&&f.fire("action-end",{event:t,interaction:this}),this.stop(),this._ending=!1},t.prototype.currentAction=function(){return this._interacting?this.prepared.name:null},t.prototype.interacting=function(){return this._interacting},t.prototype.stop=function(){f.fire("stop",{interaction:this}),this._interacting&&(f.fire("stop-active",{interaction:this}),f.fire("stop-"+this.prepared.name,{interaction:this})),this.target=this.element=null,this._interacting=!1,this.prepared.name=this.prevEvent=null},t.prototype.getPointerIndex=function(t){return"mouse"===this.pointerType||"pen"===this.pointerType?0:this.pointerIds.indexOf(c.getPointerId(t))},t.prototype.updatePointer=function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:e&&/(down|start)$/i.test(e.type),r=c.getPointerId(t),i=this.getPointerIndex(t);return-1===i&&(i=this.pointerIds.length,this.pointerIds[i]=r),n&&f.fire("update-pointer-down",{pointer:t,event:e,down:n,pointerId:r,pointerIndex:i,interaction:this}),this.pointers[i]=t,i},t.prototype.removePointer=function(t,e){var n=this.getPointerIndex(t);-1!==n&&(f.fire("remove-pointer",{pointer:t,event:e,pointerIndex:n,interaction:this}),this.pointers.splice(n,1),this.pointerIds.splice(n,1),this.downTargets.splice(n,1),this.downTimes.splice(n,1))},t.prototype._updateEventTargets=function(t,e){this._eventTarget=t,this._curEventTarget=e},t}(),y=0;y<g.length;y++){var x=g[y];v[x]=i(x)}var b={},w=p.pEventTypes;u.PointerEvent?(b[w.down]=v.pointerDown,b[w.move]=v.pointerMove,b[w.up]=v.pointerUp,b[w.cancel]=v.pointerUp):(b.mousedown=v.pointerDown,b.mousemove=v.pointerMove,b.mouseup=v.pointerUp,b.touchstart=v.pointerDown,b.touchmove=v.pointerMove,b.touchend=v.pointerUp,b.touchcancel=v.pointerUp),b.blur=o,f.on("update-pointer-down",function(t){var e=t.interaction,n=t.pointer,r=t.pointerId,i=t.pointerIndex,o=t.event,s=t.eventTarget,a=t.down;e.pointerIds[i]=r,e.pointers[i]=n,a&&(e.pointerIsDown=!0),e.interacting()||(c.setCoords(e.startCoords,e.pointers),c.copyCoords(e.curCoords,e.startCoords),c.copyCoords(e.prevCoords,e.startCoords),e.downEvent=o,e.downTimes[i]=e.curCoords.timeStamp,e.downTargets[i]=s||o&&c.getEventTargets(o)[0],e.pointerWasMoved=!1,c.pointerExtend(e.downPointer,n))}),a.signals.on("add-document",s),a.signals.on("remove-document",s),m.pointerMoveTolerance=1,m.doOnInteractions=i,m.endAll=o,m.signals=f,m.docEvents=b,a.endAllInteractions=o,e.exports=m},{"./scope":33,"./utils":44,"./utils/Signals":34,"./utils/browser":36,"./utils/domObjects":38,"./utils/events":40,"./utils/interactionFinder":45}],6:[function(t,e,n){"use strict";function r(t,e,n,r){var i=t.prepared.name,s=new o(t,e,i,n,t.element,null,r);t.target.fire(s),t.prevEvent=s}var i=t("../Interaction"),o=t("../InteractEvent"),s={firePrepared:r,names:[],methodDict:{}};i.signals.on("action-start",function(t){var e=t.interaction,n=t.event;e._interacting=!0,r(e,n,"start")}),i.signals.on("action-move",function(t){var e=t.interaction;if(r(e,t.event,"move",t.preEnd),!e.interacting())return!1}),i.signals.on("action-end",function(t){r(t.interaction,t.event,"end")}),e.exports=s},{"../InteractEvent":3,"../Interaction":5}],7:[function(t,e,n){"use strict";var r=t("./base"),i=t("../utils"),o=t("../InteractEvent"),s=t("../Interactable"),a=t("../Interaction"),c=t("../defaultOptions"),l={defaults:{enabled:!1,mouseButtons:null,origin:null,snap:null,restrict:null,inertia:null,autoScroll:null,startAxis:"xy",lockAxis:"xy"},checker:function(t,e,n){var r=n.options.drag;return r.enabled?{name:"drag",axis:"start"===r.lockAxis?r.startAxis:r.lockAxis}:null},getCursor:function(){return"move"}};a.signals.on("before-action-move",function(t){var e=t.interaction;if("drag"===e.prepared.name){var n=e.prepared.axis;"x"===n?(e.curCoords.page.y=e.startCoords.page.y,e.curCoords.client.y=e.startCoords.client.y,e.pointerDelta.page.speed=Math.abs(e.pointerDelta.page.vx),e.pointerDelta.client.speed=Math.abs(e.pointerDelta.client.vx),e.pointerDelta.client.vy=0,e.pointerDelta.page.vy=0):"y"===n&&(e.curCoords.page.x=e.startCoords.page.x,e.curCoords.client.x=e.startCoords.client.x,e.pointerDelta.page.speed=Math.abs(e.pointerDelta.page.vy),e.pointerDelta.client.speed=Math.abs(e.pointerDelta.client.vy),e.pointerDelta.client.vx=0,e.pointerDelta.page.vx=0)}}),o.signals.on("new",function(t){var e=t.iEvent,n=t.interaction;if("dragmove"===e.type){var r=n.prepared.axis;"x"===r?(e.pageY=n.startCoords.page.y,e.clientY=n.startCoords.client.y,e.dy=0):"y"===r&&(e.pageX=n.startCoords.page.x,e.clientX=n.startCoords.client.x,e.dx=0)}}),s.prototype.draggable=function(t){return i.is.object(t)?(this.options.drag.enabled=!1!==t.enabled,this.setPerAction("drag",t),this.setOnEvents("drag",t),/^(xy|x|y|start)$/.test(t.lockAxis)&&(this.options.drag.lockAxis=t.lockAxis),/^(xy|x|y)$/.test(t.startAxis)&&(this.options.drag.startAxis=t.startAxis),this):i.is.bool(t)?(this.options.drag.enabled=t,t||(this.ondragstart=this.ondragstart=this.ondragend=null),this):this.options.drag},r.drag=l,r.names.push("drag"),i.merge(s.eventTypes,["dragstart","dragmove","draginertiastart","draginertiaresume","dragend"]),r.methodDict.drag="draggable",c.drag=l.defaults,e.exports=l},{"../InteractEvent":3,"../Interactable":4,"../Interaction":5,"../defaultOptions":18,"../utils":44,"./base":6}],8:[function(t,e,n){"use strict";function r(t,e){for(var n=[],r=[],i=0;i<u.interactables.length;i++){var o;o=u.interactables[i];var s=o;if(s.options.drop.enabled){var a=s.options.drop.accept;if(!(p.is.element(a)&&a!==e||p.is.string(a)&&!p.matchesSelector(e,a)))for(var c=p.is.string(s.target)?s._context.querySelectorAll(s.target):[s.target],l=0;l<c.length;l++){var d;d=c[l];var f=d;f!==e&&(n.push(s),r.push(f))}}}return{elements:r,dropzones:n}}function i(t,e){for(var n=void 0,r=0;r<t.dropzones.length;r++){var i=t.dropzones[r],o=t.elements[r];o!==n&&(e.target=o,i.fire(e)),n=o}}function o(t,e){var n=r(t,e);t.dropzones=n.dropzones,t.elements=n.elements,t.rects=[];for(var i=0;i<t.dropzones.length;i++)t.rects[i]=t.dropzones[i].getRect(t.elements[i])}function s(t,e,n){var r=t.interaction,i=[];y&&o(r.activeDrops,n);for(var s=0;s<r.activeDrops.dropzones.length;s++){var a=r.activeDrops.dropzones[s],c=r.activeDrops.elements[s],l=r.activeDrops.rects[s];i.push(a.dropCheck(t,e,r.target,n,c,l)?c:null)}var u=p.indexOfDeepestElement(i);return{dropzone:r.activeDrops.dropzones[u]||null,element:r.activeDrops.elements[u]||null}}function a(t,e,n){var r={enter:null,leave:null,activate:null,deactivate:null,move:null,drop:null},i={dragEvent:n,interaction:t,target:t.dropElement,dropzone:t.dropTarget,relatedTarget:n.target,draggable:n.interactable,timeStamp:n.timeStamp};return t.dropElement!==t.prevDropElement&&(t.prevDropTarget&&(r.leave=p.extend({type:"dragleave"},i),n.dragLeave=r.leave.target=t.prevDropElement,n.prevDropzone=r.leave.dropzone=t.prevDropTarget),t.dropTarget&&(r.enter={dragEvent:n,interaction:t,target:t.dropElement,dropzone:t.dropTarget,relatedTarget:n.target,draggable:n.interactable,timeStamp:n.timeStamp,type:"dragenter"},n.dragEnter=t.dropElement,n.dropzone=t.dropTarget)),"dragend"===n.type&&t.dropTarget&&(r.drop=p.extend({type:"drop"},i),n.dropzone=t.dropTarget,n.relatedTarget=t.dropElement),"dragstart"===n.type&&(r.activate=p.extend({type:"dropactivate"},i),r.activate.target=null,r.activate.dropzone=null),"dragend"===n.type&&(r.deactivate=p.extend({type:"dropdeactivate"},i),r.deactivate.target=null,r.deactivate.dropzone=null),"dragmove"===n.type&&t.dropTarget&&(r.move=p.extend({dragmove:n,type:"dropmove"},i),n.dropzone=t.dropTarget),r}function c(t,e){var n=t.activeDrops,r=t.prevDropTarget,o=t.dropTarget,s=t.dropElement;e.leave&&r.fire(e.leave),e.move&&o.fire(e.move),e.enter&&o.fire(e.enter),e.drop&&o.fire(e.drop),e.deactivate&&i(n,e.deactivate),t.prevDropTarget=o,t.prevDropElement=s}var l=t("./base"),p=t("../utils"),u=t("../scope"),d=t("../interact"),f=t("../InteractEvent"),v=t("../Interactable"),g=t("../Interaction"),h=t("../defaultOptions"),m={defaults:{enabled:!1,accept:null,overlap:"pointer"}},y=!1;g.signals.on("action-start",function(t){var e=t.interaction,n=t.event;if("drag"===e.prepared.name){e.activeDrops.dropzones=[],e.activeDrops.elements=[],e.activeDrops.rects=[],e.dropEvents=null,e.dynamicDrop||o(e.activeDrops,e.element);var r=e.prevEvent,s=a(e,n,r);s.activate&&i(e.activeDrops,s.activate)}}),f.signals.on("new",function(t){var e=t.interaction,n=t.iEvent,r=t.event;if("dragmove"===n.type||"dragend"===n.type){var i=e.element,o=n,c=s(o,r,i);e.dropTarget=c.dropzone,e.dropElement=c.element,e.dropEvents=a(e,r,o)}}),g.signals.on("action-move",function(t){var e=t.interaction;"drag"===e.prepared.name&&c(e,e.dropEvents)}),g.signals.on("action-end",function(t){var e=t.interaction;"drag"===e.prepared.name&&c(e,e.dropEvents)}),g.signals.on("stop-drag",function(t){var e=t.interaction;e.activeDrops={dropzones:null,elements:null,rects:null},e.dropEvents=null}),v.prototype.dropzone=function(t){return p.is.object(t)?(this.options.drop.enabled=!1!==t.enabled,p.is.function(t.ondrop)&&(this.events.ondrop=t.ondrop),p.is.function(t.ondropactivate)&&(this.events.ondropactivate=t.ondropactivate),p.is.function(t.ondropdeactivate)&&(this.events.ondropdeactivate=t.ondropdeactivate),p.is.function(t.ondragenter)&&(this.events.ondragenter=t.ondragenter),p.is.function(t.ondragleave)&&(this.events.ondragleave=t.ondragleave),p.is.function(t.ondropmove)&&(this.events.ondropmove=t.ondropmove),/^(pointer|center)$/.test(t.overlap)?this.options.drop.overlap=t.overlap:p.is.number(t.overlap)&&(this.options.drop.overlap=Math.max(Math.min(1,t.overlap),0)),"accept"in t&&(this.options.drop.accept=t.accept),"checker"in t&&(this.options.drop.checker=t.checker),this):p.is.bool(t)?(this.options.drop.enabled=t,t||(this.ondragenter=this.ondragleave=this.ondrop=this.ondropactivate=this.ondropdeactivate=null),this):this.options.drop},v.prototype.dropCheck=function(t,e,n,r,i,o){var s=!1;if(!(o=o||this.getRect(i)))return!!this.options.drop.checker&&this.options.drop.checker(t,e,s,this,i,n,r);var a=this.options.drop.overlap;if("pointer"===a){var c=p.getOriginXY(n,r,"drag"),l=p.getPageXY(t);l.x+=c.x,l.y+=c.y;var u=l.x>o.left&&l.x<o.right,d=l.y>o.top&&l.y<o.bottom;s=u&&d}var f=n.getRect(r);if(f&&"center"===a){var v=f.left+f.width/2,g=f.top+f.height/2;s=v>=o.left&&v<=o.right&&g>=o.top&&g<=o.bottom}if(f&&p.is.number(a)){s=Math.max(0,Math.min(o.right,f.right)-Math.max(o.left,f.left))*Math.max(0,Math.min(o.bottom,f.bottom)-Math.max(o.top,f.top))/(f.width*f.height)>=a}return this.options.drop.checker&&(s=this.options.drop.checker(t,e,s,this,i,n,r)),s},v.signals.on("unset",function(t){t.interactable.dropzone(!1)}),v.settingsMethods.push("dropChecker"),g.signals.on("new",function(t){t.dropTarget=null,t.dropElement=null,t.prevDropTarget=null,t.prevDropElement=null,t.dropEvents=null,t.activeDrops={dropzones:[],elements:[],rects:[]}}),g.signals.on("stop",function(t){var e=t.interaction;e.dropTarget=e.dropElement=e.prevDropTarget=e.prevDropElement=null}),d.dynamicDrop=function(t){return p.is.bool(t)?(y=t,d):y},p.merge(v.eventTypes,["dragenter","dragleave","dropactivate","dropdeactivate","dropmove","drop"]),l.methodDict.drop="dropzone",h.drop=m.defaults,e.exports=m},{"../InteractEvent":3,"../Interactable":4,"../Interaction":5,"../defaultOptions":18,"../interact":21,"../scope":33,"../utils":44,"./base":6}],9:[function(t,e,n){"use strict";var r=t("./base"),i=t("../utils"),o=t("../InteractEvent"),s=t("../Interactable"),a=t("../Interaction"),c=t("../defaultOptions"),l={defaults:{enabled:!1,origin:null,restrict:null},checker:function(t,e,n,r,i){return i.pointerIds.length>=2?{name:"gesture"}:null},getCursor:function(){return""}};o.signals.on("new",function(t){var e=t.iEvent,n=t.interaction;"gesturestart"===e.type&&(e.ds=0,n.gesture.startDistance=n.gesture.prevDistance=e.distance,n.gesture.startAngle=n.gesture.prevAngle=e.angle,n.gesture.scale=1)}),o.signals.on("new",function(t){var e=t.iEvent,n=t.interaction;"gesturemove"===e.type&&(e.ds=e.scale-n.gesture.scale,n.target.fire(e),n.gesture.prevAngle=e.angle,n.gesture.prevDistance=e.distance,e.scale===1/0||null===e.scale||void 0===e.scale||isNaN(e.scale)||(n.gesture.scale=e.scale))}),s.prototype.gesturable=function(t){return i.is.object(t)?(this.options.gesture.enabled=!1!==t.enabled,this.setPerAction("gesture",t),this.setOnEvents("gesture",t),this):i.is.bool(t)?(this.options.gesture.enabled=t,t||(this.ongesturestart=this.ongesturestart=this.ongestureend=null),this):this.options.gesture},o.signals.on("set-delta",function(t){var e=t.interaction,n=t.iEvent,r=t.action,s=t.event,a=t.starting,c=t.ending,l=t.deltaSource;if("gesture"===r){var p=e.pointers;n.touches=[p[0],p[1]],a?(n.distance=i.touchDistance(p,l),n.box=i.touchBBox(p),n.scale=1,n.ds=0,n.angle=i.touchAngle(p,void 0,l),n.da=0):c||s instanceof o?(n.distance=e.prevEvent.distance,n.box=e.prevEvent.box,n.scale=e.prevEvent.scale,n.ds=n.scale-1,n.angle=e.prevEvent.angle,n.da=n.angle-e.gesture.startAngle):(n.distance=i.touchDistance(p,l),n.box=i.touchBBox(p),n.scale=n.distance/e.gesture.startDistance,n.angle=i.touchAngle(p,e.gesture.prevAngle,l),n.ds=n.scale-e.gesture.prevScale,n.da=n.angle-e.gesture.prevAngle)}}),a.signals.on("new",function(t){t.gesture={start:{x:0,y:0},startDistance:0,prevDistance:0,distance:0,scale:1,startAngle:0,prevAngle:0}}),r.gesture=l,r.names.push("gesture"),i.merge(s.eventTypes,["gesturestart","gesturemove","gestureend"]),r.methodDict.gesture="gesturable",c.gesture=l.defaults,e.exports=l},{"../InteractEvent":3,"../Interactable":4,"../Interaction":5,"../defaultOptions":18,"../utils":44,"./base":6}],10:[function(t,e,n){"use strict";function r(t,e,n,r,i,s,a){if(!e)return!1;if(!0===e){var c=o.is.number(s.width)?s.width:s.right-s.left,l=o.is.number(s.height)?s.height:s.bottom-s.top;if(c<0&&("left"===t?t="right":"right"===t&&(t="left")),l<0&&("top"===t?t="bottom":"bottom"===t&&(t="top")),"left"===t)return n.x<(c>=0?s.left:s.right)+a;if("top"===t)return n.y<(l>=0?s.top:s.bottom)+a;if("right"===t)return n.x>(c>=0?s.right:s.left)-a;if("bottom"===t)return n.y>(l>=0?s.bottom:s.top)-a}return!!o.is.element(r)&&(o.is.element(e)?e===r:o.matchesUpTo(r,e,i))}var i=t("./base"),o=t("../utils"),s=t("../utils/browser"),a=t("../InteractEvent"),c=t("../Interactable"),l=t("../Interaction"),p=t("../defaultOptions"),u=s.supportsTouch||s.supportsPointerEvent?20:10,d={defaults:{enabled:!1,mouseButtons:null,origin:null,snap:null,restrict:null,inertia:null,autoScroll:null,square:!1,preserveAspectRatio:!1,axis:"xy",margin:NaN,edges:null,invert:"none"},checker:function(t,e,n,i,s,a){if(!a)return null;var c=o.extend({},s.curCoords.page),l=n.options;if(l.resize.enabled){var p=l.resize,d={left:!1,right:!1,top:!1,bottom:!1};if(o.is.object(p.edges)){for(var f in d)d[f]=r(f,p.edges[f],c,s._eventTarget,i,a,p.margin||u);if(d.left=d.left&&!d.right,d.top=d.top&&!d.bottom,d.left||d.right||d.top||d.bottom)return{name:"resize",edges:d}}else{var v="y"!==l.resize.axis&&c.x>a.right-u,g="x"!==l.resize.axis&&c.y>a.bottom-u;if(v||g)return{name:"resize",axes:(v?"x":"")+(g?"y":"")}}}return null},cursors:s.isIe9?{x:"e-resize",y:"s-resize",xy:"se-resize",top:"n-resize",left:"w-resize",bottom:"s-resize",right:"e-resize",topleft:"se-resize",bottomright:"se-resize",topright:"ne-resize",bottomleft:"ne-resize"}:{x:"ew-resize",y:"ns-resize",xy:"nwse-resize",top:"ns-resize",left:"ew-resize",bottom:"ns-resize",right:"ew-resize",topleft:"nwse-resize",bottomright:"nwse-resize",topright:"nesw-resize",bottomleft:"nesw-resize"},getCursor:function(t){if(t.axis)return d.cursors[t.name+t.axis];if(t.edges){for(var e="",n=["top","bottom","left","right"],r=0;r<4;r++)t.edges[n[r]]&&(e+=n[r]);return d.cursors[e]}}};a.signals.on("new",function(t){var e=t.iEvent,n=t.interaction;if("resizestart"===e.type&&n.prepared.edges){var r=n.target.getRect(n.element),i=n.target.options.resize;if(i.square||i.preserveAspectRatio){var s=o.extend({},n.prepared.edges);s.top=s.top||s.left&&!s.bottom,s.left=s.left||s.top&&!s.right,s.bottom=s.bottom||s.right&&!s.top,s.right=s.right||s.bottom&&!s.left,n.prepared._linkedEdges=s}else n.prepared._linkedEdges=null;i.preserveAspectRatio&&(n.resizeStartAspectRatio=r.width/r.height),n.resizeRects={start:r,current:o.extend({},r),inverted:o.extend({},r),previous:o.extend({},r),delta:{left:0,right:0,width:0,top:0,bottom:0,height:0}},e.rect=n.resizeRects.inverted,e.deltaRect=n.resizeRects.delta}}),a.signals.on("new",function(t){var e=t.iEvent,n=t.phase,r=t.interaction;if("move"===n&&r.prepared.edges){var i=r.target.options.resize,s=i.invert,a="reposition"===s||"negate"===s,c=r.prepared.edges,l=r.resizeRects.start,p=r.resizeRects.current,u=r.resizeRects.inverted,d=r.resizeRects.delta,f=o.extend(r.resizeRects.previous,u),v=c,g=e.dx,h=e.dy;if(i.preserveAspectRatio||i.square){var m=i.preserveAspectRatio?r.resizeStartAspectRatio:1;c=r.prepared._linkedEdges,v.left&&v.bottom||v.right&&v.top?h=-g/m:v.left||v.right?h=g/m:(v.top||v.bottom)&&(g=h*m)}if(c.top&&(p.top+=h),c.bottom&&(p.bottom+=h),c.left&&(p.left+=g),c.right&&(p.right+=g),a){if(o.extend(u,p),"reposition"===s){var y=void 0;u.top>u.bottom&&(y=u.top,u.top=u.bottom,u.bottom=y),u.left>u.right&&(y=u.left,u.left=u.right,u.right=y)}}else u.top=Math.min(p.top,l.bottom),u.bottom=Math.max(p.bottom,l.top),u.left=Math.min(p.left,l.right),u.right=Math.max(p.right,l.left);u.width=u.right-u.left,u.height=u.bottom-u.top;for(var x in u)d[x]=u[x]-f[x];e.edges=r.prepared.edges,e.rect=u,e.deltaRect=d}}),c.prototype.resizable=function(t){return o.is.object(t)?(this.options.resize.enabled=!1!==t.enabled,this.setPerAction("resize",t),this.setOnEvents("resize",t),/^x$|^y$|^xy$/.test(t.axis)?this.options.resize.axis=t.axis:null===t.axis&&(this.options.resize.axis=p.resize.axis),o.is.bool(t.preserveAspectRatio)?this.options.resize.preserveAspectRatio=t.preserveAspectRatio:o.is.bool(t.square)&&(this.options.resize.square=t.square),this):o.is.bool(t)?(this.options.resize.enabled=t,t||(this.onresizestart=this.onresizestart=this.onresizeend=null),this):this.options.resize},l.signals.on("new",function(t){t.resizeAxes="xy"}),a.signals.on("set-delta",function(t){var e=t.interaction,n=t.iEvent;"resize"===t.action&&e.resizeAxes&&(e.target.options.resize.square?("y"===e.resizeAxes?n.dx=n.dy:n.dy=n.dx,n.axes="xy"):(n.axes=e.resizeAxes,"x"===e.resizeAxes?n.dy=0:"y"===e.resizeAxes&&(n.dx=0)))}),i.resize=d,i.names.push("resize"), +o.merge(c.eventTypes,["resizestart","resizemove","resizeinertiastart","resizeinertiaresume","resizeend"]),i.methodDict.resize="resizable",p.resize=d.defaults,e.exports=d},{"../InteractEvent":3,"../Interactable":4,"../Interaction":5,"../defaultOptions":18,"../utils":44,"../utils/browser":36,"./base":6}],11:[function(t,e,n){"use strict";var r=t("./utils/raf"),i=t("./utils/window").getWindow,o=t("./utils/is"),s=t("./utils/domUtils"),a=t("./Interaction"),c=t("./defaultOptions"),l={defaults:{enabled:!1,container:null,margin:60,speed:300},interaction:null,i:null,x:0,y:0,isScrolling:!1,prevTime:0,start:function(t){l.isScrolling=!0,r.cancel(l.i),l.interaction=t,l.prevTime=(new Date).getTime(),l.i=r.request(l.scroll)},stop:function(){l.isScrolling=!1,r.cancel(l.i)},scroll:function(){var t=l.interaction.target.options[l.interaction.prepared.name].autoScroll,e=t.container||i(l.interaction.element),n=(new Date).getTime(),s=(n-l.prevTime)/1e3,a=t.speed*s;a>=1&&(o.window(e)?e.scrollBy(l.x*a,l.y*a):e&&(e.scrollLeft+=l.x*a,e.scrollTop+=l.y*a),l.prevTime=n),l.isScrolling&&(r.cancel(l.i),l.i=r.request(l.scroll))},check:function(t,e){var n=t.options;return n[e].autoScroll&&n[e].autoScroll.enabled},onInteractionMove:function(t){var e=t.interaction,n=t.pointer;if(e.interacting()&&l.check(e.target,e.prepared.name)){if(e.simulation)return void(l.x=l.y=0);var r=void 0,a=void 0,c=void 0,p=void 0,u=e.target.options[e.prepared.name].autoScroll,d=u.container||i(e.element);if(o.window(d))p=n.clientX<l.margin,r=n.clientY<l.margin,a=n.clientX>d.innerWidth-l.margin,c=n.clientY>d.innerHeight-l.margin;else{var f=s.getElementClientRect(d);p=n.clientX<f.left+l.margin,r=n.clientY<f.top+l.margin,a=n.clientX>f.right-l.margin,c=n.clientY>f.bottom-l.margin}l.x=a?1:p?-1:0,l.y=c?1:r?-1:0,l.isScrolling||(l.margin=u.margin,l.speed=u.speed,l.start(e))}}};a.signals.on("stop-active",function(){l.stop()}),a.signals.on("action-move",l.onInteractionMove),c.perAction.autoScroll=l.defaults,e.exports=l},{"./Interaction":5,"./defaultOptions":18,"./utils/domUtils":39,"./utils/is":46,"./utils/raf":50,"./utils/window":52}],12:[function(t,e,n){"use strict";var r=t("../Interactable"),i=t("../actions/base"),o=t("../utils/is"),s=t("../utils/domUtils"),a=t("../utils"),c=a.warnOnce;r.prototype.getAction=function(t,e,n,r){var i=this.defaultActionChecker(t,e,n,r);return this.options.actionChecker?this.options.actionChecker(t,e,i,this,r,n):i},r.prototype.ignoreFrom=c(function(t){return this._backCompatOption("ignoreFrom",t)},"Interactable.ignoreForm() has been deprecated. Use Interactble.draggable({ignoreFrom: newValue})."),r.prototype.allowFrom=c(function(t){return this._backCompatOption("allowFrom",t)},"Interactable.allowForm() has been deprecated. Use Interactble.draggable({allowFrom: newValue})."),r.prototype.testIgnore=function(t,e,n){return!(!t||!o.element(n))&&(o.string(t)?s.matchesUpTo(n,t,e):!!o.element(t)&&s.nodeContains(t,n))},r.prototype.testAllow=function(t,e,n){return!t||!!o.element(n)&&(o.string(t)?s.matchesUpTo(n,t,e):!!o.element(t)&&s.nodeContains(t,n))},r.prototype.testIgnoreAllow=function(t,e,n){return!this.testIgnore(t.ignoreFrom,e,n)&&this.testAllow(t.allowFrom,e,n)},r.prototype.actionChecker=function(t){return o.function(t)?(this.options.actionChecker=t,this):null===t?(delete this.options.actionChecker,this):this.options.actionChecker},r.prototype.styleCursor=function(t){return o.bool(t)?(this.options.styleCursor=t,this):null===t?(delete this.options.styleCursor,this):this.options.styleCursor},r.prototype.defaultActionChecker=function(t,e,n,r){for(var o=this.getRect(r),s=e.buttons||{0:1,1:4,3:8,4:16}[e.button],a=null,c=0;c<i.names.length;c++){var l;l=i.names[c];var p=l;if((!n.pointerIsDown||!/mouse|pointer/.test(n.pointerType)||0!=(s&this.options[p].mouseButtons))&&(a=i[p].checker(t,e,this,r,n,o)))return a}}},{"../Interactable":4,"../actions/base":6,"../utils":44,"../utils/domUtils":39,"../utils/is":46}],13:[function(t,e,n){"use strict";function r(t,e,n,r){return v.is.object(t)&&e.testIgnoreAllow(e.options[t.name],n,r)&&e.options[t.name].enabled&&a(e,n,t)?t:null}function i(t,e,n,i,o,s){for(var a=0,c=i.length;a<c;a++){var l=i[a],p=o[a],u=r(l.getAction(e,n,t,p),l,p,s);if(u)return{action:u,target:l,element:p}}return{}}function o(t,e,n,r){function o(t){s.push(t),a.push(c)}for(var s=[],a=[],c=r;v.is.element(c);){s=[],a=[],f.interactables.forEachMatch(c,o);var l=i(t,e,n,s,a,r);if(l.action&&!l.target.options[l.action.name].manualStart)return l;c=v.parentNode(c)}return{}}function s(t,e){var n=e.action,r=e.target,i=e.element;if(n=n||{},t.target&&t.target.options.styleCursor&&(t.target._doc.documentElement.style.cursor=""),t.target=r,t.element=i,v.copyAction(t.prepared,n),r&&r.options.styleCursor){var o=n?u[n.name].getCursor(n):"";t.target._doc.documentElement.style.cursor=o}g.fire("prepared",{interaction:t})}function a(t,e,n){var r=t.options,i=r[n.name].max,o=r[n.name].maxPerElement,s=0,a=0,c=0;if(i&&o&&h.maxInteractions){for(var l=0;l<f.interactions.length;l++){var p;p=f.interactions[l];var u=p,d=u.prepared.name;if(u.interacting()){if(++s>=h.maxInteractions)return!1;if(u.target===t){if((a+=d===n.name|0)>=i)return!1;if(u.element===e&&(c++,d!==n.name||c>=o))return!1}}}return h.maxInteractions>0}}var c=t("../interact"),l=t("../Interactable"),p=t("../Interaction"),u=t("../actions/base"),d=t("../defaultOptions"),f=t("../scope"),v=t("../utils"),g=t("../utils/Signals").new();t("./InteractableMethods");var h={signals:g,withinInteractionLimit:a,maxInteractions:1/0,defaults:{perAction:{manualStart:!1,max:1/0,maxPerElement:1,allowFrom:null,ignoreFrom:null,mouseButtons:1}},setActionDefaults:function(t){v.extend(t.defaults,h.defaults.perAction)},validateAction:r};p.signals.on("down",function(t){var e=t.interaction,n=t.pointer,r=t.event,i=t.eventTarget;if(!e.interacting()){s(e,o(e,n,r,i))}}),p.signals.on("move",function(t){var e=t.interaction,n=t.pointer,r=t.event,i=t.eventTarget;if("mouse"===e.pointerType&&!e.pointerIsDown&&!e.interacting()){s(e,o(e,n,r,i))}}),p.signals.on("move",function(t){var e=t.interaction,n=t.event;if(e.pointerIsDown&&!e.interacting()&&e.pointerWasMoved&&e.prepared.name){g.fire("before-start",t);var r=e.target;e.prepared.name&&r&&(r.options[e.prepared.name].manualStart||!a(r,e.element,e.prepared)?e.stop(n):e.start(e.prepared,r,e.element))}}),p.signals.on("stop",function(t){var e=t.interaction,n=e.target;n&&n.options.styleCursor&&(n._doc.documentElement.style.cursor="")}),c.maxInteractions=function(t){return v.is.number(t)?(h.maxInteractions=t,c):h.maxInteractions},l.settingsMethods.push("styleCursor"),l.settingsMethods.push("actionChecker"),l.settingsMethods.push("ignoreFrom"),l.settingsMethods.push("allowFrom"),d.base.actionChecker=null,d.base.styleCursor=!0,v.extend(d.perAction,h.defaults.perAction),e.exports=h},{"../Interactable":4,"../Interaction":5,"../actions/base":6,"../defaultOptions":18,"../interact":21,"../scope":33,"../utils":44,"../utils/Signals":34,"./InteractableMethods":12}],14:[function(t,e,n){"use strict";function r(t,e){if(!e)return!1;var n=e.options.drag.startAxis;return"xy"===t||"xy"===n||n===t}var i=t("./base"),o=t("../scope"),s=t("../utils/is"),a=t("../utils/domUtils"),c=a.parentNode;i.setActionDefaults(t("../actions/drag")),i.signals.on("before-start",function(t){var e=t.interaction,n=t.eventTarget,a=t.dx,l=t.dy;if("drag"===e.prepared.name){var p=Math.abs(a),u=Math.abs(l),d=e.target.options.drag,f=d.startAxis,v=p>u?"x":p<u?"y":"xy";if(e.prepared.axis="start"===d.lockAxis?v[0]:d.lockAxis,"xy"!==v&&"xy"!==f&&f!==v){e.prepared.name=null;for(var g=n,h=function(t){if(t!==e.target){var o=e.target.options.drag;if(!o.manualStart&&t.testIgnoreAllow(o,g,n)){var s=t.getAction(e.downPointer,e.downEvent,e,g);if(s&&"drag"===s.name&&r(v,t)&&i.validateAction(s,t,g,n))return t}}};s.element(g);){var m=o.interactables.forEachMatch(g,h);if(m){e.prepared.name="drag",e.target=m,e.element=g;break}g=c(g)}}}})},{"../actions/drag":7,"../scope":33,"../utils/domUtils":39,"../utils/is":46,"./base":13}],15:[function(t,e,n){"use strict";t("./base").setActionDefaults(t("../actions/gesture"))},{"../actions/gesture":9,"./base":13}],16:[function(t,e,n){"use strict";function r(t){var e=t.prepared&&t.prepared.name;if(!e)return null;var n=t.target.options;return n[e].hold||n[e].delay}var i=t("./base"),o=t("../Interaction");i.defaults.perAction.hold=0,i.defaults.perAction.delay=0,o.signals.on("new",function(t){t.autoStartHoldTimer=null}),i.signals.on("prepared",function(t){var e=t.interaction,n=r(e);n>0&&(e.autoStartHoldTimer=setTimeout(function(){e.start(e.prepared,e.target,e.element)},n))}),o.signals.on("move",function(t){var e=t.interaction,n=t.duplicate;e.pointerWasMoved&&!n&&clearTimeout(e.autoStartHoldTimer)}),i.signals.on("before-start",function(t){var e=t.interaction;r(e)>0&&(e.prepared.name=null)}),e.exports={getHoldDuration:r}},{"../Interaction":5,"./base":13}],17:[function(t,e,n){"use strict";t("./base").setActionDefaults(t("../actions/resize"))},{"../actions/resize":10,"./base":13}],18:[function(t,e,n){"use strict";e.exports={base:{accept:null,preventDefault:"auto",deltaSource:"page"},perAction:{origin:{x:0,y:0},inertia:{enabled:!1,resistance:10,minSpeed:100,endSpeed:10,allowResume:!0,smoothEndDuration:300}}}},{}],19:[function(t,e,n){"use strict";t("./inertia"),t("./modifiers/snap"),t("./modifiers/restrict"),t("./pointerEvents/base"),t("./pointerEvents/holdRepeat"),t("./pointerEvents/interactableTargets"),t("./autoStart/hold"),t("./actions/gesture"),t("./actions/resize"),t("./actions/drag"),t("./actions/drop"),t("./modifiers/snapSize"),t("./modifiers/restrictEdges"),t("./modifiers/restrictSize"),t("./autoStart/gesture"),t("./autoStart/resize"),t("./autoStart/drag"),t("./interactablePreventDefault.js"),t("./autoScroll"),e.exports=t("./interact")},{"./actions/drag":7,"./actions/drop":8,"./actions/gesture":9,"./actions/resize":10,"./autoScroll":11,"./autoStart/drag":14,"./autoStart/gesture":15,"./autoStart/hold":16,"./autoStart/resize":17,"./inertia":20,"./interact":21,"./interactablePreventDefault.js":22,"./modifiers/restrict":24,"./modifiers/restrictEdges":25,"./modifiers/restrictSize":26,"./modifiers/snap":27,"./modifiers/snapSize":28,"./pointerEvents/base":30,"./pointerEvents/holdRepeat":31,"./pointerEvents/interactableTargets":32}],20:[function(t,e,n){"use strict";function r(t,e){var n=t.target.options[t.prepared.name].inertia,r=n.resistance,i=-Math.log(n.endSpeed/e.v0)/r;e.x0=t.prevEvent.pageX,e.y0=t.prevEvent.pageY,e.t0=e.startEvent.timeStamp/1e3,e.sx=e.sy=0,e.modifiedXe=e.xe=(e.vx0-i)/r,e.modifiedYe=e.ye=(e.vy0-i)/r,e.te=i,e.lambda_v0=r/e.v0,e.one_ve_v0=1-n.endSpeed/e.v0}function i(){s(this),p.setCoordDeltas(this.pointerDelta,this.prevCoords,this.curCoords);var t=this.inertiaStatus,e=this.target.options[this.prepared.name].inertia,n=e.resistance,r=(new Date).getTime()/1e3-t.t0;if(r<t.te){var i=1-(Math.exp(-n*r)-t.lambda_v0)/t.one_ve_v0;if(t.modifiedXe===t.xe&&t.modifiedYe===t.ye)t.sx=t.xe*i,t.sy=t.ye*i;else{var o=p.getQuadraticCurvePoint(0,0,t.xe,t.ye,t.modifiedXe,t.modifiedYe,i);t.sx=o.x,t.sy=o.y}this.doMove(),t.i=u.request(this.boundInertiaFrame)}else t.sx=t.modifiedXe,t.sy=t.modifiedYe,this.doMove(),this.end(t.startEvent),t.active=!1,this.simulation=null;p.copyCoords(this.prevCoords,this.curCoords)}function o(){s(this);var t=this.inertiaStatus,e=(new Date).getTime()-t.t0,n=this.target.options[this.prepared.name].inertia.smoothEndDuration;e<n?(t.sx=p.easeOutQuad(e,0,t.xe,n),t.sy=p.easeOutQuad(e,0,t.ye,n),this.pointerMove(t.startEvent,t.startEvent),t.i=u.request(this.boundSmoothEndFrame)):(t.sx=t.xe,t.sy=t.ye,this.pointerMove(t.startEvent,t.startEvent),this.end(t.startEvent),t.smoothEnd=t.active=!1,this.simulation=null)}function s(t){var e=t.inertiaStatus;if(e.active){var n=e.upCoords.page,r=e.upCoords.client;p.setCoords(t.curCoords,[{pageX:n.x+e.sx,pageY:n.y+e.sy,clientX:r.x+e.sx,clientY:r.y+e.sy}])}}var a=t("./InteractEvent"),c=t("./Interaction"),l=t("./modifiers/base"),p=t("./utils"),u=t("./utils/raf");c.signals.on("new",function(t){t.inertiaStatus={active:!1,smoothEnd:!1,allowResume:!1,startEvent:null,upCoords:{},xe:0,ye:0,sx:0,sy:0,t0:0,vx0:0,vys:0,duration:0,lambda_v0:0,one_ve_v0:0,i:null},t.boundInertiaFrame=function(){return i.apply(t)},t.boundSmoothEndFrame=function(){return o.apply(t)}}),c.signals.on("down",function(t){var e=t.interaction,n=t.event,r=t.pointer,i=t.eventTarget,o=e.inertiaStatus;if(o.active)for(var s=i;p.is.element(s);){if(s===e.element){u.cancel(o.i),o.active=!1,e.simulation=null,e.updatePointer(r),p.setCoords(e.curCoords,e.pointers);var d={interaction:e};c.signals.fire("before-action-move",d),c.signals.fire("action-resume",d);var f=new a(e,n,e.prepared.name,"inertiaresume",e.element);e.target.fire(f),e.prevEvent=f,l.resetStatuses(e.modifierStatuses),p.copyCoords(e.prevCoords,e.curCoords);break}s=p.parentNode(s)}}),c.signals.on("up",function(t){var e=t.interaction,n=t.event,i=e.inertiaStatus;if(e.interacting()&&!i.active){var o=e.target,s=o&&o.options,c=s&&e.prepared.name&&s[e.prepared.name].inertia,d=(new Date).getTime(),f={},v=p.extend({},e.curCoords.page),g=e.pointerDelta.client.speed,h=!1,m=void 0,y=c&&c.enabled&&"gesture"!==e.prepared.name&&n!==i.startEvent,x=y&&d-e.curCoords.timeStamp<50&&g>c.minSpeed&&g>c.endSpeed,b={interaction:e,pageCoords:v,statuses:f,preEnd:!0,requireEndOnly:!0};y&&!x&&(l.resetStatuses(f),m=l.setAll(b),m.shouldMove&&m.locked&&(h=!0)),(x||h)&&(p.copyCoords(i.upCoords,e.curCoords),e.pointers[0]=i.startEvent=new a(e,n,e.prepared.name,"inertiastart",e.element),i.t0=d,i.active=!0,i.allowResume=c.allowResume,e.simulation=i,o.fire(i.startEvent),x?(i.vx0=e.pointerDelta.client.vx,i.vy0=e.pointerDelta.client.vy,i.v0=g,r(e,i),p.extend(v,e.curCoords.page),v.x+=i.xe,v.y+=i.ye,l.resetStatuses(f),m=l.setAll(b),i.modifiedXe+=m.dx,i.modifiedYe+=m.dy,i.i=u.request(e.boundInertiaFrame)):(i.smoothEnd=!0,i.xe=m.dx,i.ye=m.dy,i.sx=i.sy=0,i.i=u.request(e.boundSmoothEndFrame)))}}),c.signals.on("stop-active",function(t){var e=t.interaction,n=e.inertiaStatus;n.active&&(u.cancel(n.i),n.active=!1,e.simulation=null)})},{"./InteractEvent":3,"./Interaction":5,"./modifiers/base":23,"./utils":44,"./utils/raf":50}],21:[function(t,e,n){"use strict";function r(t,e){var n=a.interactables.get(t,e);return n||(n=new c(t,e),n.events.global=p),n}var i=t("./utils/browser"),o=t("./utils/events"),s=t("./utils"),a=t("./scope"),c=t("./Interactable"),l=t("./Interaction"),p={};r.isSet=function(t,e){return-1!==a.interactables.indexOfElement(t,e&&e.context)},r.on=function(t,e,n){if(s.is.string(t)&&-1!==t.search(" ")&&(t=t.trim().split(/ +/)),s.is.array(t)){for(var i=0;i<t.length;i++){var l;l=t[i];var u=l;r.on(u,e,n)}return r}if(s.is.object(t)){for(var d in t)r.on(d,t[d],e);return r}return s.contains(c.eventTypes,t)?p[t]?p[t].push(e):p[t]=[e]:o.add(a.document,t,e,{options:n}),r},r.off=function(t,e,n){if(s.is.string(t)&&-1!==t.search(" ")&&(t=t.trim().split(/ +/)),s.is.array(t)){for(var i=0;i<t.length;i++){var l;l=t[i];var u=l;r.off(u,e,n)}return r}if(s.is.object(t)){for(var d in t)r.off(d,t[d],e);return r}if(s.contains(c.eventTypes,t)){var f=void 0;t in p&&-1!==(f=p[t].indexOf(e))&&p[t].splice(f,1)}else o.remove(a.document,t,e,n);return r},r.debug=function(){return a},r.getPointerAverage=s.pointerAverage,r.getTouchBBox=s.touchBBox,r.getTouchDistance=s.touchDistance,r.getTouchAngle=s.touchAngle,r.getElementRect=s.getElementRect,r.getElementClientRect=s.getElementClientRect,r.matchesSelector=s.matchesSelector,r.closest=s.closest,r.supportsTouch=function(){return i.supportsTouch},r.supportsPointerEvent=function(){return i.supportsPointerEvent},r.stop=function(t){for(var e=a.interactions.length-1;e>=0;e--)a.interactions[e].stop(t);return r},r.pointerMoveTolerance=function(t){return s.is.number(t)?(l.pointerMoveTolerance=t,r):l.pointerMoveTolerance},r.addDocument=a.addDocument,r.removeDocument=a.removeDocument,a.interact=r,e.exports=r},{"./Interactable":4,"./Interaction":5,"./scope":33,"./utils":44,"./utils/browser":36,"./utils/events":40}],22:[function(t,e,n){"use strict";function r(t){var e=t.interaction,n=t.event;e.target&&e.target.checkAndPreventDefault(n)}var i=t("./Interactable"),o=t("./Interaction"),s=t("./scope"),a=t("./utils/is"),c=t("./utils/events"),l=t("./utils/browser"),p=t("./utils/domUtils"),u=p.nodeContains,d=p.matchesSelector;i.prototype.preventDefault=function(t){return/^(always|never|auto)$/.test(t)?(this.options.preventDefault=t,this):a.bool(t)?(this.options.preventDefault=t?"always":"never",this):this.options.preventDefault},i.prototype.checkAndPreventDefault=function(t){var e=this.options.preventDefault;if("never"!==e)return"always"===e?void t.preventDefault():void(c.supportsPassive&&/^touch(start|move)$/.test(t.type)&&!l.isIOS||/^(mouse|pointer|touch)*(down|start)/i.test(t.type)||a.element(t.target)&&d(t.target,"input,select,textarea,[contenteditable=true],[contenteditable=true] *")||t.preventDefault())};for(var f=["down","move","up","cancel"],v=0;v<f.length;v++){var g=f[v];o.signals.on(g,r)}o.docEvents.dragstart=function(t){for(var e=0;e<s.interactions.length;e++){var n;n=s.interactions[e];var r=n;if(r.element&&(r.element===t.target||u(r.element,t.target)))return void r.target.checkAndPreventDefault(t)}}},{"./Interactable":4,"./Interaction":5,"./scope":33,"./utils/browser":36,"./utils/domUtils":39,"./utils/events":40,"./utils/is":46}],23:[function(t,e,n){"use strict";function r(t,e,n){return t&&t.enabled&&(e||!t.endOnly)&&(!n||t.endOnly)}var i=t("../InteractEvent"),o=t("../Interaction"),s=t("../utils/extend"),a={names:[],setOffsets:function(t){var e=t.interaction,n=t.pageCoords,r=e.target,i=e.element,o=e.startOffset,s=r.getRect(i);s?(o.left=n.x-s.left,o.top=n.y-s.top,o.right=s.right-n.x,o.bottom=s.bottom-n.y,"width"in s||(s.width=s.right-s.left),"height"in s||(s.height=s.bottom-s.top)):o.left=o.top=o.right=o.bottom=0,t.rect=s,t.interactable=r,t.element=i;for(var c=0;c<a.names.length;c++){var l;l=a.names[c];var p=l;t.options=r.options[e.prepared.name][p],t.options&&(e.modifierOffsets[p]=a[p].setOffset(t))}},setAll:function(t){var e=t.interaction,n=t.statuses,i=t.preEnd,o=t.requireEndOnly,c={dx:0,dy:0,changed:!1,locked:!1,shouldMove:!0};t.modifiedCoords=s({},t.pageCoords);for(var l=0;l<a.names.length;l++){var p;p=a.names[l];var u=p,d=a[u],f=e.target.options[e.prepared.name][u];r(f,i,o)&&(t.status=t.status=n[u],t.options=f,t.offset=t.interaction.modifierOffsets[u],d.set(t),t.status.locked&&(t.modifiedCoords.x+=t.status.dx,t.modifiedCoords.y+=t.status.dy,c.dx+=t.status.dx,c.dy+=t.status.dy,c.locked=!0))}return c.shouldMove=!t.status||!c.locked||t.status.changed,c},resetStatuses:function(t){for(var e=0;e<a.names.length;e++){var n;n=a.names[e];var r=n,i=t[r]||{};i.dx=i.dy=0,i.modifiedX=i.modifiedY=NaN,i.locked=!1,i.changed=!0,t[r]=i}return t},start:function(t,e){var n=t.interaction,r={interaction:n,pageCoords:("action-resume"===e?n.curCoords:n.startCoords).page,startOffset:n.startOffset,statuses:n.modifierStatuses,preEnd:!1,requireEndOnly:!1};a.setOffsets(r),a.resetStatuses(r.statuses),r.pageCoords=s({},n.startCoords.page),n.modifierResult=a.setAll(r)},beforeMove:function(t){var e=t.interaction,n=t.preEnd,r=t.interactingBeforeMove,i=a.setAll({interaction:e,preEnd:n,pageCoords:e.curCoords.page,statuses:e.modifierStatuses,requireEndOnly:!1});!i.shouldMove&&r&&(e._dontFireMove=!0),e.modifierResult=i},end:function(t){for(var e=t.interaction,n=t.event,i=0;i<a.names.length;i++){var o;o=a.names[i];var s=o;if(r(e.target.options[e.prepared.name][s],!0,!0)){e.doMove({event:n,preEnd:!0});break}}},setXY:function(t){for(var e=t.iEvent,n=t.interaction,r=s({},t),i=0;i<a.names.length;i++){var o=a.names[i];if(r.options=n.target.options[n.prepared.name][o],r.options){var c=a[o];r.status=n.modifierStatuses[o],e[o]=c.modifyCoords(r)}}}};o.signals.on("new",function(t){t.startOffset={left:0,right:0,top:0,bottom:0},t.modifierOffsets={},t.modifierStatuses=a.resetStatuses({}),t.modifierResult=null}),o.signals.on("action-start",a.start),o.signals.on("action-resume",a.start),o.signals.on("before-action-move",a.beforeMove),o.signals.on("action-end",a.end),i.signals.on("set-xy",a.setXY),e.exports=a},{"../InteractEvent":3,"../Interaction":5,"../utils/extend":41}],24:[function(t,e,n){"use strict";function r(t,e,n){return o.is.function(t)?o.resolveRectLike(t,e.target,e.element,[n.x,n.y,e]):o.resolveRectLike(t,e.target,e.element)}var i=t("./base"),o=t("../utils"),s=t("../defaultOptions"),a={defaults:{enabled:!1,endOnly:!1,restriction:null,elementRect:null},setOffset:function(t){var e=t.rect,n=t.startOffset,r=t.options,i=r&&r.elementRect,o={};return e&&i?(o.left=n.left-e.width*i.left,o.top=n.top-e.height*i.top,o.right=n.right-e.width*(1-i.right),o.bottom=n.bottom-e.height*(1-i.bottom)):o.left=o.top=o.right=o.bottom=0,o},set:function(t){var e=t.modifiedCoords,n=t.interaction,i=t.status,s=t.options;if(!s)return i;var a=i.useStatusXY?{x:i.x,y:i.y}:o.extend({},e),c=r(s.restriction,n,a);if(!c)return i;i.dx=0,i.dy=0,i.locked=!1;var l=c,p=a.x,u=a.y,d=n.modifierOffsets.restrict;"x"in c&&"y"in c?(p=Math.max(Math.min(l.x+l.width-d.right,a.x),l.x+d.left),u=Math.max(Math.min(l.y+l.height-d.bottom,a.y),l.y+d.top)):(p=Math.max(Math.min(l.right-d.right,a.x),l.left+d.left),u=Math.max(Math.min(l.bottom-d.bottom,a.y),l.top+d.top)),i.dx=p-a.x,i.dy=u-a.y,i.changed=i.modifiedX!==p||i.modifiedY!==u,i.locked=!(!i.dx&&!i.dy),i.modifiedX=p,i.modifiedY=u},modifyCoords:function(t){var e=t.page,n=t.client,r=t.status,i=t.phase,o=t.options,s=o&&o.elementRect;if(o&&o.enabled&&("start"!==i||!s||!r.locked)&&r.locked)return e.x+=r.dx,e.y+=r.dy,n.x+=r.dx,n.y+=r.dy,{dx:r.dx,dy:r.dy}},getRestrictionRect:r};i.restrict=a,i.names.push("restrict"),s.perAction.restrict=a.defaults,e.exports=a},{"../defaultOptions":18,"../utils":44,"./base":23}],25:[function(t,e,n){"use strict";var r=t("./base"),i=t("../utils"),o=t("../utils/rect"),s=t("../defaultOptions"),a=t("../actions/resize"),c=t("./restrict"),l=c.getRestrictionRect,p={top:1/0,left:1/0,bottom:-1/0,right:-1/0},u={top:-1/0,left:-1/0,bottom:1/0,right:1/0},d={defaults:{enabled:!1,endOnly:!1,min:null,max:null,offset:null},setOffset:function(t){var e=t.interaction,n=t.startOffset,r=t.options;if(!r)return i.extend({},n);var o=l(r.offset,e,e.startCoords.page);return o?{top:n.top+o.y,left:n.left+o.x,bottom:n.bottom+o.y,right:n.right+o.x}:n},set:function(t){var e=t.modifiedCoords,n=t.interaction,r=t.status,s=t.offset,a=t.options,c=n.prepared.linkedEdges||n.prepared.edges;if(n.interacting()&&c){var d=r.useStatusXY?{x:r.x,y:r.y}:i.extend({},e),f=o.xywhToTlbr(l(a.inner,n,d))||p,v=o.xywhToTlbr(l(a.outer,n,d))||u,g=d.x,h=d.y;r.dx=0,r.dy=0,r.locked=!1,c.top?h=Math.min(Math.max(v.top+s.top,d.y),f.top+s.top):c.bottom&&(h=Math.max(Math.min(v.bottom-s.bottom,d.y),f.bottom-s.bottom)),c.left?g=Math.min(Math.max(v.left+s.left,d.x),f.left+s.left):c.right&&(g=Math.max(Math.min(v.right-s.right,d.x),f.right-s.right)),r.dx=g-d.x,r.dy=h-d.y,r.changed=r.modifiedX!==g||r.modifiedY!==h,r.locked=!(!r.dx&&!r.dy),r.modifiedX=g,r.modifiedY=h}},modifyCoords:function(t){var e=t.page,n=t.client,r=t.status,i=t.phase,o=t.options;if(o&&o.enabled&&("start"!==i||!r.locked)&&r.locked)return e.x+=r.dx,e.y+=r.dy,n.x+=r.dx,n.y+=r.dy,{dx:r.dx,dy:r.dy}},noInner:p,noOuter:u,getRestrictionRect:l};r.restrictEdges=d,r.names.push("restrictEdges"),s.perAction.restrictEdges=d.defaults,a.defaults.restrictEdges=d.defaults,e.exports=d},{"../actions/resize":10,"../defaultOptions":18,"../utils":44,"../utils/rect":51,"./base":23,"./restrict":24}],26:[function(t,e,n){"use strict";var r=t("./base"),i=t("./restrictEdges"),o=t("../utils"),s=t("../utils/rect"),a=t("../defaultOptions"),c=t("../actions/resize"),l={width:-1/0,height:-1/0},p={width:1/0,height:1/0},u={defaults:{enabled:!1,endOnly:!1,min:null,max:null},setOffset:function(t){return t.interaction.startOffset},set:function(t){var e=t.interaction,n=t.options,r=e.prepared.linkedEdges||e.prepared.edges;if(e.interacting()&&r){var a=s.xywhToTlbr(e.resizeRects.inverted),c=s.tlbrToXywh(i.getRestrictionRect(n.min,e))||l,u=s.tlbrToXywh(i.getRestrictionRect(n.max,e))||p;t.options={enabled:n.enabled,endOnly:n.endOnly,inner:o.extend({},i.noInner),outer:o.extend({},i.noOuter)},r.top?(t.options.inner.top=a.bottom-c.height,t.options.outer.top=a.bottom-u.height):r.bottom&&(t.options.inner.bottom=a.top+c.height,t.options.outer.bottom=a.top+u.height),r.left?(t.options.inner.left=a.right-c.width,t.options.outer.left=a.right-u.width):r.right&&(t.options.inner.right=a.left+c.width,t.options.outer.right=a.left+u.width),i.set(t)}},modifyCoords:i.modifyCoords};r.restrictSize=u,r.names.push("restrictSize"),a.perAction.restrictSize=u.defaults,c.defaults.restrictSize=u.defaults,e.exports=u},{"../actions/resize":10,"../defaultOptions":18,"../utils":44,"../utils/rect":51,"./base":23,"./restrictEdges":25}],27:[function(t,e,n){"use strict";var r=t("./base"),i=t("../interact"),o=t("../utils"),s=t("../defaultOptions"),a={defaults:{enabled:!1,endOnly:!1,range:1/0,targets:null,offsets:null,relativePoints:null},setOffset:function(t){var e=t.interaction,n=t.interactable,r=t.element,i=t.rect,s=t.startOffset,a=t.options,c=[],l=o.rectToXY(o.resolveRectLike(a.origin)),p=l||o.getOriginXY(n,r,e.prepared.name);a=a||n.options[e.prepared.name].snap||{};var u=void 0;if("startCoords"===a.offset)u={x:e.startCoords.page.x-p.x,y:e.startCoords.page.y-p.y};else{var d=o.resolveRectLike(a.offset,n,r,[e]);u=o.rectToXY(d)||{x:0,y:0}}if(i&&a.relativePoints&&a.relativePoints.length)for(var f=0;f<a.relativePoints.length;f++){var v;v=a.relativePoints[f];var g=v,h=g.x,m=g.y;c.push({x:s.left-i.width*h+u.x,y:s.top-i.height*m+u.y})}else c.push(u);return c},set:function(t){var e=t.interaction,n=t.modifiedCoords,r=t.status,i=t.options,s=t.offset,a=[],c=void 0,l=void 0,p=void 0;if(r.useStatusXY)l={x:r.x,y:r.y};else{var u=o.getOriginXY(e.target,e.element,e.prepared.name);l=o.extend({},n),l.x-=u.x,l.y-=u.y}r.realX=l.x,r.realY=l.y;for(var d=i.targets?i.targets.length:0,f=0;f<s.length;f++){var v;v=s[f];for(var g=v,h=g.x,m=g.y,y=l.x-h,x=l.y-m,b=0;b<(i.targets||[]).length;b++){var w;w=(i.targets||[])[b];var E=w;c=o.is.function(E)?E(y,x,e):E,c&&a.push({x:o.is.number(c.x)?c.x+h:y,y:o.is.number(c.y)?c.y+m:x,range:o.is.number(c.range)?c.range:i.range})}}var T={target:null,inRange:!1,distance:0,range:0,dx:0,dy:0};for(p=0,d=a.length;p<d;p++){c=a[p];var S=c.range,C=c.x-l.x,I=c.y-l.y,D=o.hypot(C,I),O=D<=S;S===1/0&&T.inRange&&T.range!==1/0&&(O=!1),T.target&&!(O?T.inRange&&S!==1/0?D/S<T.distance/T.range:S===1/0&&T.range!==1/0||D<T.distance:!T.inRange&&D<T.distance)||(T.target=c,T.distance=D,T.range=S,T.inRange=O,T.dx=C,T.dy=I,r.range=S)}var M=void 0;T.target?(M=r.modifiedX!==T.target.x||r.modifiedY!==T.target.y,r.modifiedX=T.target.x,r.modifiedY=T.target.y):(M=!0,r.modifiedX=NaN,r.modifiedY=NaN),r.dx=T.dx,r.dy=T.dy,r.changed=M||T.inRange&&!r.locked,r.locked=T.inRange},modifyCoords:function(t){var e=t.page,n=t.client,r=t.status,i=t.phase,o=t.options,s=o&&o.relativePoints;if(o&&o.enabled&&("start"!==i||!s||!s.length))return r.locked&&(e.x+=r.dx,e.y+=r.dy,n.x+=r.dx,n.y+=r.dy),{range:r.range,locked:r.locked,x:r.modifiedX,y:r.modifiedY,realX:r.realX,realY:r.realY,dx:r.dx,dy:r.dy}}};i.createSnapGrid=function(t){return function(e,n){var r=t.limits||{left:-1/0,right:1/0,top:-1/0,bottom:1/0},i=0,s=0;o.is.object(t.offset)&&(i=t.offset.x,s=t.offset.y);var a=Math.round((e-i)/t.x),c=Math.round((n-s)/t.y);return{x:Math.max(r.left,Math.min(r.right,a*t.x+i)),y:Math.max(r.top,Math.min(r.bottom,c*t.y+s)),range:t.range}}},r.snap=a,r.names.push("snap"),s.perAction.snap=a.defaults,e.exports=a},{"../defaultOptions":18,"../interact":21,"../utils":44,"./base":23}],28:[function(t,e,n){"use strict";var r=t("./base"),i=t("./snap"),o=t("../defaultOptions"),s=t("../actions/resize"),a=t("../utils/"),c={defaults:{enabled:!1,endOnly:!1,range:1/0,targets:null,offsets:null},setOffset:function(t){var e=t.interaction,n=t.options,r=e.prepared.edges;if(r){t.options={relativePoints:[{x:r.left?0:1,y:r.top?0:1}],origin:{x:0,y:0},offset:"self",range:n.range};var o=i.setOffset(t);return t.options=n,o}},set:function(t){var e=t.interaction,n=t.options,r=t.offset,o=t.modifiedCoords,s=a.extend({},o),c=s.x-r[0].x,l=s.y-r[0].y;t.options=a.extend({},n),t.options.targets=[];for(var p=0;p<(n.targets||[]).length;p++){var u;u=(n.targets||[])[p];var d=u,f=void 0;f=a.is.function(d)?d(c,l,e):d,f&&("width"in f&&"height"in f&&(f.x=f.width,f.y=f.height),t.options.targets.push(f))}i.set(t)},modifyCoords:function(t){var e=t.options;t.options=a.extend({},e),t.options.enabled=e.enabled,t.options.relativePoints=[null],i.modifyCoords(t)}};r.snapSize=c,r.names.push("snapSize"),o.perAction.snapSize=c.defaults,s.defaults.snapSize=c.defaults,e.exports=c},{"../actions/resize":10,"../defaultOptions":18,"../utils/":44,"./base":23,"./snap":27}],29:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=t("../utils/pointerUtils");e.exports=function(){function t(e,n,o,s,a){if(r(this,t),i.pointerExtend(this,o),o!==n&&i.pointerExtend(this,n),this.interaction=a,this.timeStamp=(new Date).getTime(),this.originalEvent=o,this.type=e,this.pointerId=i.getPointerId(n),this.pointerType=i.getPointerType(n),this.target=s,this.currentTarget=null,"tap"===e){var c=a.getPointerIndex(n);this.dt=this.timeStamp-a.downTimes[c];var l=this.timeStamp-a.tapTime;this.double=!!(a.prevTap&&"doubletap"!==a.prevTap.type&&a.prevTap.target===this.target&&l<500)}else"doubletap"===e&&(this.dt=n.timeStamp-a.tapTime)}return t.prototype.subtractOrigin=function(t){var e=t.x,n=t.y;return this.pageX-=e,this.pageY-=n,this.clientX-=e,this.clientY-=n,this},t.prototype.addOrigin=function(t){var e=t.x,n=t.y;return this.pageX+=e,this.pageY+=n,this.clientX+=e,this.clientY+=n,this},t.prototype.preventDefault=function(){this.originalEvent.preventDefault()},t.prototype.stopPropagation=function(){this.propagationStopped=!0},t.prototype.stopImmediatePropagation=function(){this.immediatePropagationStopped=this.propagationStopped=!0},t}()},{"../utils/pointerUtils":49}],30:[function(t,e,n){"use strict";function r(t){for(var e=t.interaction,n=t.pointer,s=t.event,c=t.eventTarget,p=t.type,u=void 0===p?t.pointerEvent.type:p,d=t.targets,f=void 0===d?i(t):d,v=t.pointerEvent,g=void 0===v?new o(u,n,s,c,e):v,h={interaction:e,pointer:n,event:s,eventTarget:c,targets:f,type:u,pointerEvent:g},m=0;m<f.length;m++){var y=f[m];for(var x in y.props||{})g[x]=y.props[x];var b=a.getOriginXY(y.eventable,y.element);if(g.subtractOrigin(b),g.eventable=y.eventable,g.currentTarget=y.element,y.eventable.fire(g),g.addOrigin(b),g.immediatePropagationStopped||g.propagationStopped&&m+1<f.length&&f[m+1].element!==g.currentTarget)break}if(l.fire("fired",h),"tap"===u){var w=g.double?r({interaction:e,pointer:n,event:s,eventTarget:c,type:"doubletap"}):g;e.prevTap=w,e.tapTime=w.timeStamp}return g}function i(t){var e=t.interaction,n=t.pointer,r=t.event,i=t.eventTarget,o=t.type,s=e.getPointerIndex(n);if("tap"===o&&(e.pointerWasMoved||!e.downTargets[s]||e.downTargets[s]!==i))return[];for(var c=a.getPath(i),p={interaction:e,pointer:n,event:r,eventTarget:i,type:o,path:c,targets:[],element:null},u=0;u<c.length;u++){var d;d=c[u];var f=d;p.element=f,l.fire("collect-targets",p)}return"hold"===o&&(p.targets=p.targets.filter(function(t){return t.eventable.options.holdDuration===e.holdTimers[s].duration})),p.targets}var o=t("./PointerEvent"),s=t("../Interaction"),a=t("../utils"),c=t("../defaultOptions"),l=t("../utils/Signals").new(),p=["down","up","cancel"],u=["down","up","cancel"],d={PointerEvent:o,fire:r,collectEventTargets:i,signals:l,defaults:{holdDuration:600,ignoreFrom:null,allowFrom:null,origin:{x:0,y:0}},types:["down","move","up","cancel","tap","doubletap","hold"]};s.signals.on("update-pointer-down",function(t){var e=t.interaction,n=t.pointerIndex;e.holdTimers[n]={duration:1/0,timeout:null}}),s.signals.on("remove-pointer",function(t){var e=t.interaction,n=t.pointerIndex;e.holdTimers.splice(n,1)}), +s.signals.on("move",function(t){var e=t.interaction,n=t.pointer,i=t.event,o=t.eventTarget,s=t.duplicateMove,a=e.getPointerIndex(n);s||e.pointerIsDown&&!e.pointerWasMoved||(e.pointerIsDown&&clearTimeout(e.holdTimers[a].timeout),r({interaction:e,pointer:n,event:i,eventTarget:o,type:"move"}))}),s.signals.on("down",function(t){for(var e=t.interaction,n=t.pointer,i=t.event,o=t.eventTarget,s=t.pointerIndex,c=e.holdTimers[s],p=a.getPath(o),u={interaction:e,pointer:n,event:i,eventTarget:o,type:"hold",targets:[],path:p,element:null},d=0;d<p.length;d++){var f;f=p[d];var v=f;u.element=v,l.fire("collect-targets",u)}if(u.targets.length){for(var g=1/0,h=0;h<u.targets.length;h++){var m;m=u.targets[h];var y=m,x=y.eventable.options.holdDuration;x<g&&(g=x)}c.duration=g,c.timeout=setTimeout(function(){r({interaction:e,eventTarget:o,pointer:n,event:i,type:"hold"})},g)}}),s.signals.on("up",function(t){var e=t.interaction,n=t.pointer,i=t.event,o=t.eventTarget;e.pointerWasMoved||r({interaction:e,eventTarget:o,pointer:n,event:i,type:"tap"})});for(var f=["up","cancel"],v=0;v<f.length;v++){var g=f[v];s.signals.on(g,function(t){var e=t.interaction,n=t.pointerIndex;e.holdTimers[n]&&clearTimeout(e.holdTimers[n].timeout)})}for(var h=0;h<p.length;h++)s.signals.on(p[h],function(t){return function(e){var n=e.interaction,i=e.pointer,o=e.event;r({interaction:n,eventTarget:e.eventTarget,pointer:i,event:o,type:t})}}(u[h]));s.signals.on("new",function(t){t.prevTap=null,t.tapTime=0,t.holdTimers=[]}),c.pointerEvents=d.defaults,e.exports=d},{"../Interaction":5,"../defaultOptions":18,"../utils":44,"../utils/Signals":34,"./PointerEvent":29}],31:[function(t,e,n){"use strict";function r(t){var e=t.pointerEvent;"hold"===e.type&&(e.count=(e.count||0)+1)}function i(t){var e=t.interaction,n=t.pointerEvent,r=t.eventTarget,i=t.targets;if("hold"===n.type&&i.length){var o=i[0].eventable.options.holdRepeatInterval;o<=0||(e.holdIntervalHandle=setTimeout(function(){s.fire({interaction:e,eventTarget:r,type:"hold",pointer:n,event:n})},o))}}function o(t){var e=t.interaction;e.holdIntervalHandle&&(clearInterval(e.holdIntervalHandle),e.holdIntervalHandle=null)}var s=t("./base"),a=t("../Interaction");s.signals.on("new",r),s.signals.on("fired",i);for(var c=["move","up","cancel","endall"],l=0;l<c.length;l++){var p=c[l];a.signals.on(p,o)}s.defaults.holdRepeatInterval=0,s.types.push("holdrepeat"),e.exports={onNew:r,onFired:i,endHoldRepeat:o}},{"../Interaction":5,"./base":30}],32:[function(t,e,n){"use strict";var r=t("./base"),i=t("../Interactable"),o=t("../utils/is"),s=t("../scope"),a=t("../utils/extend"),c=t("../utils/arr"),l=c.merge;r.signals.on("collect-targets",function(t){var e=t.targets,n=t.element,r=t.type,i=t.eventTarget;s.interactables.forEachMatch(n,function(t){var s=t.events,a=s.options;s[r]&&o.element(n)&&t.testIgnoreAllow(a,n,i)&&e.push({element:n,eventable:s,props:{interactable:t}})})}),i.signals.on("new",function(t){var e=t.interactable;e.events.getRect=function(t){return e.getRect(t)}}),i.signals.on("set",function(t){var e=t.interactable,n=t.options;a(e.events.options,r.defaults),a(e.events.options,n)}),l(i.eventTypes,r.types),i.prototype.pointerEvents=function(t){return a(this.events.options,t),this};var p=i.prototype._backCompatOption;i.prototype._backCompatOption=function(t,e){var n=p.call(this,t,e);return n===this&&(this.events.options[t]=e),n},i.settingsMethods.push("pointerEvents")},{"../Interactable":4,"../scope":33,"../utils/arr":35,"../utils/extend":41,"../utils/is":46,"./base":30}],33:[function(t,e,n){"use strict";var r=t("./utils"),i=t("./utils/events"),o=t("./utils/Signals").new(),s=t("./utils/window"),a=s.getWindow,c={signals:o,events:i,utils:r,document:t("./utils/domObjects").document,documents:[],addDocument:function(t,e){if(r.contains(c.documents,t))return!1;e=e||a(t),c.documents.push(t),i.documents.push(t),t!==c.document&&i.add(e,"unload",c.onWindowUnload),o.fire("add-document",{doc:t,win:e})},removeDocument:function(t,e){var n=c.documents.indexOf(t);e=e||a(t),i.remove(e,"unload",c.onWindowUnload),c.documents.splice(n,1),i.documents.splice(n,1),o.fire("remove-document",{win:e,doc:t})},onWindowUnload:function(){c.removeDocument(this.document,this)}};e.exports=c},{"./utils":44,"./utils/Signals":34,"./utils/domObjects":38,"./utils/events":40,"./utils/window":52}],34:[function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=function(){function t(){r(this,t),this.listeners={}}return t.prototype.on=function(t,e){if(!this.listeners[t])return void(this.listeners[t]=[e]);this.listeners[t].push(e)},t.prototype.off=function(t,e){if(this.listeners[t]){var n=this.listeners[t].indexOf(e);-1!==n&&this.listeners[t].splice(n,1)}},t.prototype.fire=function(t,e){var n=this.listeners[t];if(n)for(var r=0;r<n.length;r++){var i;i=n[r];var o=i;if(!1===o(e,t))return}},t}();i.new=function(){return new i},e.exports=i},{}],35:[function(t,e,n){"use strict";function r(t,e){return-1!==t.indexOf(e)}function i(t,e){for(var n=0;n<e.length;n++){var r;r=e[n];var i=r;t.push(i)}return t}e.exports={contains:r,merge:i}},{}],36:[function(t,e,n){"use strict";var r=t("./window"),i=r.window,o=t("./is"),s=t("./domObjects"),a=s.Element,c=i.navigator,l={supportsTouch:!!("ontouchstart"in i||o.function(i.DocumentTouch)&&s.document instanceof i.DocumentTouch),supportsPointerEvent:!!s.PointerEvent,isIOS:/iP(hone|od|ad)/.test(c.platform),isIOS7:/iP(hone|od|ad)/.test(c.platform)&&/OS 7[^\d]/.test(c.appVersion),isIe9:/MSIE 9/.test(c.userAgent),prefixedMatchesSelector:"matches"in a.prototype?"matches":"webkitMatchesSelector"in a.prototype?"webkitMatchesSelector":"mozMatchesSelector"in a.prototype?"mozMatchesSelector":"oMatchesSelector"in a.prototype?"oMatchesSelector":"msMatchesSelector",pEventTypes:s.PointerEvent?s.PointerEvent===i.MSPointerEvent?{up:"MSPointerUp",down:"MSPointerDown",over:"mouseover",out:"mouseout",move:"MSPointerMove",cancel:"MSPointerCancel"}:{up:"pointerup",down:"pointerdown",over:"pointerover",out:"pointerout",move:"pointermove",cancel:"pointercancel"}:null,wheelEvent:"onmousewheel"in s.document?"mousewheel":"wheel"};l.isOperaMobile="Opera"===c.appName&&l.supportsTouch&&c.userAgent.match("Presto"),e.exports=l},{"./domObjects":38,"./is":46,"./window":52}],37:[function(t,e,n){"use strict";var r=t("./is");e.exports=function t(e){var n={};for(var i in e)r.plainObject(e[i])?n[i]=t(e[i]):n[i]=e[i];return n}},{"./is":46}],38:[function(t,e,n){"use strict";function r(){}var i={},o=t("./window").window;i.document=o.document,i.DocumentFragment=o.DocumentFragment||r,i.SVGElement=o.SVGElement||r,i.SVGSVGElement=o.SVGSVGElement||r,i.SVGElementInstance=o.SVGElementInstance||r,i.Element=o.Element||r,i.HTMLElement=o.HTMLElement||i.Element,i.Event=o.Event,i.Touch=o.Touch||r,i.PointerEvent=o.PointerEvent||o.MSPointerEvent,e.exports=i},{"./window":52}],39:[function(t,e,n){"use strict";var r=t("./window"),i=t("./browser"),o=t("./is"),s=t("./domObjects"),a={nodeContains:function(t,e){for(;e;){if(e===t)return!0;e=e.parentNode}return!1},closest:function(t,e){for(;o.element(t);){if(a.matchesSelector(t,e))return t;t=a.parentNode(t)}return null},parentNode:function(t){var e=t.parentNode;if(o.docFrag(e)){for(;(e=e.host)&&o.docFrag(e););return e}return e},matchesSelector:function(t,e){return r.window!==r.realWindow&&(e=e.replace(/\/deep\//g," ")),t[i.prefixedMatchesSelector](e)},indexOfDeepestElement:function(t){var e=[],n=[],r=void 0,i=t[0],o=i?0:-1,a=void 0,c=void 0,l=void 0,p=void 0;for(l=1;l<t.length;l++)if((r=t[l])&&r!==i)if(i){if(r.parentNode!==r.ownerDocument)if(i.parentNode!==r.ownerDocument){if(!e.length)for(a=i;a.parentNode&&a.parentNode!==a.ownerDocument;)e.unshift(a),a=a.parentNode;if(i instanceof s.HTMLElement&&r instanceof s.SVGElement&&!(r instanceof s.SVGSVGElement)){if(r===i.parentNode)continue;a=r.ownerSVGElement}else a=r;for(n=[];a.parentNode!==a.ownerDocument;)n.unshift(a),a=a.parentNode;for(p=0;n[p]&&n[p]===e[p];)p++;var u=[n[p-1],n[p],e[p]];for(c=u[0].lastChild;c;){if(c===u[1]){i=r,o=l,e=[];break}if(c===u[2])break;c=c.previousSibling}}else i=r,o=l}else i=r,o=l;return o},matchesUpTo:function(t,e,n){for(;o.element(t);){if(a.matchesSelector(t,e))return!0;if((t=a.parentNode(t))===n)return a.matchesSelector(t,e)}return!1},getActualElement:function(t){return t instanceof s.SVGElementInstance?t.correspondingUseElement:t},getScrollXY:function(t){return t=t||r.window,{x:t.scrollX||t.document.documentElement.scrollLeft,y:t.scrollY||t.document.documentElement.scrollTop}},getElementClientRect:function(t){var e=t instanceof s.SVGElement?t.getBoundingClientRect():t.getClientRects()[0];return e&&{left:e.left,right:e.right,top:e.top,bottom:e.bottom,width:e.width||e.right-e.left,height:e.height||e.bottom-e.top}},getElementRect:function(t){var e=a.getElementClientRect(t);if(!i.isIOS7&&e){var n=a.getScrollXY(r.getWindow(t));e.left+=n.x,e.right+=n.x,e.top+=n.y,e.bottom+=n.y}return e},getPath:function(t){for(var e=[];t;)e.push(t),t=a.parentNode(t);return e},trySelector:function(t){return!!o.string(t)&&(s.document.querySelector(t),!0)}};e.exports=a},{"./browser":36,"./domObjects":38,"./is":46,"./window":52}],40:[function(t,e,n){"use strict";function r(t,e,n,r){var i=p(r),o=x.indexOf(t),s=b[o];s||(s={events:{},typeCount:0},o=x.push(t)-1,b.push(s)),s.events[e]||(s.events[e]=[],s.typeCount++),y(s.events[e],n)||(t.addEventListener(e,n,T?i:!!i.capture),s.events[e].push(n))}function i(t,e,n,r){var o=p(r),s=x.indexOf(t),a=b[s];if(a&&a.events)if("all"!==e){if(a.events[e]){var c=a.events[e].length;if("all"===n){for(var l=0;l<c;l++)i(t,e,a.events[e][l],o);return}for(var u=0;u<c;u++)if(a.events[e][u]===n){t.removeEventListener("on"+e,n,T?o:!!o.capture),a.events[e].splice(u,1);break}a.events[e]&&0===a.events[e].length&&(a.events[e]=null,a.typeCount--)}a.typeCount||(b.splice(s,1),x.splice(s,1))}else for(e in a.events)a.events.hasOwnProperty(e)&&i(t,e,"all")}function o(t,e,n,i,o){var s=p(o);if(!w[n]){w[n]={selectors:[],contexts:[],listeners:[]};for(var l=0;l<E.length;l++){var u=E[l];r(u,n,a),r(u,n,c,!0)}}var d=w[n],f=void 0;for(f=d.selectors.length-1;f>=0&&(d.selectors[f]!==t||d.contexts[f]!==e);f--);-1===f&&(f=d.selectors.length,d.selectors.push(t),d.contexts.push(e),d.listeners.push([])),d.listeners[f].push([i,!!s.capture,s.passive])}function s(t,e,n,r,o){var s=p(o),l=w[n],u=!1,d=void 0;if(l)for(d=l.selectors.length-1;d>=0;d--)if(l.selectors[d]===t&&l.contexts[d]===e){for(var f=l.listeners[d],v=f.length-1;v>=0;v--){var g=f[v],h=g[0],m=g[1],y=g[2];if(h===r&&m===!!s.capture&&y===s.passive){f.splice(v,1),f.length||(l.selectors.splice(d,1),l.contexts.splice(d,1),l.listeners.splice(d,1),i(e,n,a),i(e,n,c,!0),l.selectors.length||(w[n]=null)),u=!0;break}}if(u)break}}function a(t,e){var n=p(e),r={},i=w[t.type],o=f.getEventTargets(t),s=o[0],a=s;for(v(r,t),r.originalEvent=t,r.preventDefault=l;u.element(a);){for(var c=0;c<i.selectors.length;c++){var g=i.selectors[c],h=i.contexts[c];if(d.matchesSelector(a,g)&&d.nodeContains(h,s)&&d.nodeContains(h,a)){var m=i.listeners[c];r.currentTarget=a;for(var y=0;y<m.length;y++){var x=m[y],b=x[0],E=x[1],T=x[2];E===!!n.capture&&T===n.passive&&b(r)}}}a=d.parentNode(a)}}function c(t){return a.call(this,t,!0)}function l(){this.originalEvent.preventDefault()}function p(t){return u.object(t)?t:{capture:t}}var u=t("./is"),d=t("./domUtils"),f=t("./pointerUtils"),v=t("./pointerExtend"),g=t("./window"),h=g.window,m=t("./arr"),y=m.contains,x=[],b=[],w={},E=[],T=function(){var t=!1;return h.document.createElement("div").addEventListener("test",null,{get capture(){t=!0}}),t}();e.exports={add:r,remove:i,addDelegate:o,removeDelegate:s,delegateListener:a,delegateUseCapture:c,delegatedEvents:w,documents:E,supportsOptions:T,_elements:x,_targets:b}},{"./arr":35,"./domUtils":39,"./is":46,"./pointerExtend":48,"./pointerUtils":49,"./window":52}],41:[function(t,e,n){"use strict";e.exports=function(t,e){for(var n in e)t[n]=e[n];return t}},{}],42:[function(t,e,n){"use strict";var r=t("./rect"),i=r.resolveRectLike,o=r.rectToXY;e.exports=function(t,e,n){var r=t.options[n],s=r&&r.origin,a=s||t.options.origin,c=i(a,t,e,[t&&e]);return o(c)||{x:0,y:0}}},{"./rect":51}],43:[function(t,e,n){"use strict";e.exports=function(t,e){return Math.sqrt(t*t+e*e)}},{}],44:[function(t,e,n){"use strict";var r=t("./extend"),i=t("./window"),o={warnOnce:function(t,e){var n=!1;return function(){return n||(i.window.console.warn(e),n=!0),t.apply(this,arguments)}},_getQBezierValue:function(t,e,n,r){var i=1-t;return i*i*e+2*i*t*n+t*t*r},getQuadraticCurvePoint:function(t,e,n,r,i,s,a){return{x:o._getQBezierValue(a,t,n,i),y:o._getQBezierValue(a,e,r,s)}},easeOutQuad:function(t,e,n,r){return t/=r,-n*t*(t-2)+e},copyAction:function(t,e){return t.name=e.name,t.axis=e.axis,t.edges=e.edges,t},is:t("./is"),extend:r,hypot:t("./hypot"),getOriginXY:t("./getOriginXY")};r(o,t("./arr")),r(o,t("./domUtils")),r(o,t("./pointerUtils")),r(o,t("./rect")),e.exports=o},{"./arr":35,"./domUtils":39,"./extend":41,"./getOriginXY":42,"./hypot":43,"./is":46,"./pointerUtils":49,"./rect":51,"./window":52}],45:[function(t,e,n){"use strict";var r=t("../scope"),i=t("./index"),o={methodOrder:["simulationResume","mouseOrPen","hasPointer","idle"],search:function(t,e,n){for(var r=i.getPointerType(t),s=i.getPointerId(t),a={pointer:t,pointerId:s,pointerType:r,eventType:e,eventTarget:n},c=0;c<o.methodOrder.length;c++){var l;l=o.methodOrder[c];var p=l,u=o[p](a);if(u)return u}},simulationResume:function(t){var e=t.pointerType,n=t.eventType,o=t.eventTarget;if(!/down|start/i.test(n))return null;for(var s=0;s<r.interactions.length;s++){var a;a=r.interactions[s];var c=a,l=o;if(c.simulation&&c.simulation.allowResume&&c.pointerType===e)for(;l;){if(l===c.element)return c;l=i.parentNode(l)}}return null},mouseOrPen:function(t){var e=t.pointerId,n=t.pointerType,o=t.eventType;if("mouse"!==n&&"pen"!==n)return null;for(var s=void 0,a=0;a<r.interactions.length;a++){var c;c=r.interactions[a];var l=c;if(l.pointerType===n){if(l.simulation&&!i.contains(l.pointerIds,e))continue;if(l.interacting())return l;s||(s=l)}}if(s)return s;for(var p=0;p<r.interactions.length;p++){var u;u=r.interactions[p];var d=u;if(!(d.pointerType!==n||/down/i.test(o)&&d.simulation))return d}return null},hasPointer:function(t){for(var e=t.pointerId,n=0;n<r.interactions.length;n++){var o;o=r.interactions[n];var s=o;if(i.contains(s.pointerIds,e))return s}},idle:function(t){for(var e=t.pointerType,n=0;n<r.interactions.length;n++){var i;i=r.interactions[n];var o=i;if(1===o.pointerIds.length){var s=o.target;if(s&&!s.options.gesture.enabled)continue}else if(o.pointerIds.length>=2)continue;if(!o.interacting()&&e===o.pointerType)return o}return null}};e.exports=o},{"../scope":33,"./index":44}],46:[function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=t("./window"),o=t("./isWindow"),s={array:function(){},window:function(t){return t===i.window||o(t)},docFrag:function(t){return s.object(t)&&11===t.nodeType},object:function(t){return!!t&&"object"===(void 0===t?"undefined":r(t))},function:function(t){return"function"==typeof t},number:function(t){return"number"==typeof t},bool:function(t){return"boolean"==typeof t},string:function(t){return"string"==typeof t},element:function(t){if(!t||"object"!==(void 0===t?"undefined":r(t)))return!1;var e=i.getWindow(t)||i.window;return/object|function/.test(r(e.Element))?t instanceof e.Element:1===t.nodeType&&"string"==typeof t.nodeName},plainObject:function(t){return s.object(t)&&"Object"===t.constructor.name}};s.array=function(t){return s.object(t)&&void 0!==t.length&&s.function(t.splice)},e.exports=s},{"./isWindow":47,"./window":52}],47:[function(t,e,n){"use strict";e.exports=function(t){return!(!t||!t.Window)&&t instanceof t.Window}},{}],48:[function(t,e,n){"use strict";function r(t,n){for(var r in n){var i=e.exports.prefixedPropREs,o=!1;for(var s in i)if(0===r.indexOf(s)&&i[s].test(r)){o=!0;break}o||"function"==typeof n[r]||(t[r]=n[r])}return t}r.prefixedPropREs={webkit:/(Movement[XY]|Radius[XY]|RotationAngle|Force)$/},e.exports=r},{}],49:[function(t,e,n){"use strict";var r=t("./hypot"),i=t("./browser"),o=t("./domObjects"),s=t("./domUtils"),a=t("./domObjects"),c=t("./is"),l=t("./pointerExtend"),p={copyCoords:function(t,e){t.page=t.page||{},t.page.x=e.page.x,t.page.y=e.page.y,t.client=t.client||{},t.client.x=e.client.x,t.client.y=e.client.y,t.timeStamp=e.timeStamp},setCoordDeltas:function(t,e,n){t.page.x=n.page.x-e.page.x,t.page.y=n.page.y-e.page.y,t.client.x=n.client.x-e.client.x,t.client.y=n.client.y-e.client.y,t.timeStamp=n.timeStamp-e.timeStamp;var i=Math.max(t.timeStamp/1e3,.001);t.page.speed=r(t.page.x,t.page.y)/i,t.page.vx=t.page.x/i,t.page.vy=t.page.y/i,t.client.speed=r(t.client.x,t.page.y)/i,t.client.vx=t.client.x/i,t.client.vy=t.client.y/i},isNativePointer:function(t){return t instanceof o.Event||t instanceof o.Touch},getXY:function(t,e,n){return n=n||{},t=t||"page",n.x=e[t+"X"],n.y=e[t+"Y"],n},getPageXY:function(t,e){return e=e||{},i.isOperaMobile&&p.isNativePointer(t)?(p.getXY("screen",t,e),e.x+=window.scrollX,e.y+=window.scrollY):p.getXY("page",t,e),e},getClientXY:function(t,e){return e=e||{},i.isOperaMobile&&p.isNativePointer(t)?p.getXY("screen",t,e):p.getXY("client",t,e),e},getPointerId:function(t){return c.number(t.pointerId)?t.pointerId:t.identifier},setCoords:function(t,e,n){var r=e.length>1?p.pointerAverage(e):e[0],i={};p.getPageXY(r,i),t.page.x=i.x,t.page.y=i.y,p.getClientXY(r,i),t.client.x=i.x,t.client.y=i.y,t.timeStamp=c.number(n)?n:(new Date).getTime()},pointerExtend:l,getTouchPair:function(t){var e=[];return c.array(t)?(e[0]=t[0],e[1]=t[1]):"touchend"===t.type?1===t.touches.length?(e[0]=t.touches[0],e[1]=t.changedTouches[0]):0===t.touches.length&&(e[0]=t.changedTouches[0],e[1]=t.changedTouches[1]):(e[0]=t.touches[0],e[1]=t.touches[1]),e},pointerAverage:function(t){for(var e={pageX:0,pageY:0,clientX:0,clientY:0,screenX:0,screenY:0},n=0;n<t.length;n++){var r;r=t[n];var i=r;for(var o in e)e[o]+=i[o]}for(var s in e)e[s]/=t.length;return e},touchBBox:function(t){if(t.length||t.touches&&t.touches.length>1){var e=p.getTouchPair(t),n=Math.min(e[0].pageX,e[1].pageX),r=Math.min(e[0].pageY,e[1].pageY);return{x:n,y:r,left:n,top:r,width:Math.max(e[0].pageX,e[1].pageX)-n,height:Math.max(e[0].pageY,e[1].pageY)-r}}},touchDistance:function(t,e){var n=e+"X",i=e+"Y",o=p.getTouchPair(t),s=o[0][n]-o[1][n],a=o[0][i]-o[1][i];return r(s,a)},touchAngle:function(t,e,n){var r=n+"X",i=n+"Y",o=p.getTouchPair(t),s=o[1][r]-o[0][r],a=o[1][i]-o[0][i];return 180*Math.atan2(a,s)/Math.PI},getPointerType:function(t){return c.string(t.pointerType)?t.pointerType:c.number(t.pointerType)?[void 0,void 0,"touch","pen","mouse"][t.pointerType]:/touch/.test(t.type)||t instanceof a.Touch?"touch":"mouse"},getEventTargets:function(t){var e=c.function(t.composedPath)?t.composedPath():t.path;return[s.getActualElement(e?e[0]:t.target),s.getActualElement(t.currentTarget)]}};e.exports=p},{"./browser":36,"./domObjects":38,"./domUtils":39,"./hypot":43,"./is":46,"./pointerExtend":48}],50:[function(t,e,n){"use strict";for(var r=t("./window"),i=r.window,o=["ms","moz","webkit","o"],s=0,a=void 0,c=void 0,l=0;l<o.length&&!i.requestAnimationFrame;l++)a=i[o[l]+"RequestAnimationFrame"],c=i[o[l]+"CancelAnimationFrame"]||i[o[l]+"CancelRequestAnimationFrame"];a||(a=function(t){var e=(new Date).getTime(),n=Math.max(0,16-(e-s)),r=setTimeout(function(){t(e+n)},n);return s=e+n,r}),c||(c=function(t){clearTimeout(t)}),e.exports={request:a,cancel:c}},{"./window":52}],51:[function(t,e,n){"use strict";var r=t("./extend"),i=t("./is"),o=t("./domUtils"),s=o.closest,a=o.parentNode,c=o.getElementRect,l={getStringOptionResult:function(t,e,n){return i.string(t)?t="parent"===t?a(n):"self"===t?e.getRect(n):s(n,t):null},resolveRectLike:function(t,e,n,r){return t=l.getStringOptionResult(t,e,n)||t,i.function(t)&&(t=t.apply(null,r)),i.element(t)&&(t=c(t)),t},rectToXY:function(t){return t&&{x:"x"in t?t.x:t.left,y:"y"in t?t.y:t.top}},xywhToTlbr:function(t){return!t||"left"in t&&"top"in t||(t=r({},t),t.left=t.x||0,t.top=t.y||0,t.right=t.right||t.left+t.width,t.bottom=t.bottom||t.top+t.height),t},tlbrToXywh:function(t){return!t||"x"in t&&"y"in t||(t=r({},t),t.x=t.left||0,t.top=t.top||0,t.width=t.width||t.right-t.x,t.height=t.height||t.bottom-t.y),t}};e.exports=l},{"./domUtils":39,"./extend":41,"./is":46}],52:[function(t,e,n){"use strict";function r(t){i.realWindow=t;var e=t.document.createTextNode("");e.ownerDocument!==t.document&&"function"==typeof t.wrap&&t.wrap(e)===e&&(t=t.wrap(t)),i.window=t}var i=e.exports,o=t("./isWindow");"undefined"==typeof window?(i.window=void 0,i.realWindow=void 0):r(window),i.getWindow=function(t){if(o(t))return t;var e=t.ownerDocument||t;return e.defaultView||e.parentWindow||i.window},i.init=r},{"./isWindow":47}]},{},[1])(1)}); + diff --git a/career/js/jquery-1.8.2.min.js b/career/js/jquery-1.8.2.min.js new file mode 100644 index 0000000..73cd249 --- /dev/null +++ b/career/js/jquery-1.8.2.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.2 jquery.com | jquery.org/license */ +(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bY(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bW.length;while(e--){b=bW[e]+c;if(b in a)return b}return d}function bZ(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function b$(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bZ(c)&&(e[f]=p._data(c,"olddisplay",cc(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b_(a,b,c){var d=bP.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function ca(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bV[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bV[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bV[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bV[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bV[e]+"Width"))||0));return f}function cb(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0||d==null){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bQ.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+ca(a,b,c||(f?"border":"content"),e)+"px"}function cc(a){if(bS[a])return bS[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cA(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cv;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cA(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cA(a,c,d,e,"*",g)),h}function cB(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cC(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cD(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cL(){try{return new a.XMLHttpRequest}catch(b){}}function cM(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cU(){return setTimeout(function(){cN=b},0),cN=p.now()}function cV(a,b){p.each(b,function(b,c){var d=(cT[b]||[]).concat(cT["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cW(a,b,c){var d,e=0,f=0,g=cS.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cN||cU(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cN||cU(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cX(k,j.opts.specialEasing);for(;e<g;e++){d=cS[e].call(j,a,k,j.opts);if(d)return d}return cV(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cX(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cY(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bZ(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cc(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cP.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cZ(a,b,c,d,e){return new cZ.prototype.init(a,b,c,d,e)}function c$(a,b){var c,d={height:a},e=0;b=b?1:0;for(;e<4;e+=2-b)c=bV[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function da(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o&&!o.call(" ")?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":(a+"").replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete")setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){var e=p.type(c);e==="function"&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&e!=="string"&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")||(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)d=p._data(g[h],a+"queueHooks"),d&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)f.indexOf(" "+b[g]+" ")<0&&(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=b+""}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,needsContext:f&&p.expr.match.needsContext.test(f),namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=k.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click"))for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){h={},j=[];for(d=0;d<q;d++)l=o[d],m=l.selector,h[m]===b&&(h[m]=l.needsContext?p(m,this).index(f)>=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){i=u[d],c.currentTarget=i.elem;for(e=0;e<i.matches.length&&!c.isImmediatePropagationStopped();e++){l=i.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,g=((p.event.special[l.origType]||{}).handle||l.handler).apply(i.elem,r),g!==b&&(c.result=g,g===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),!V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length===1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h<i;h++)if(f=a[h])if(!c||c(f,d,e))g.push(f),j&&b.push(h);return g}function bl(a,b,c,d,e,f){return d&&!d[o]&&(d=bl(d)),e&&!e[o]&&(e=bl(e,f)),z(function(f,g,h,i){if(f&&e)return;var j,k,l,m=[],n=[],o=g.length,p=f||bo(b||"*",h.nodeType?[h]:h,[],f),q=a&&(f||!b)?bk(p,m,a,h,i):p,r=c?e||(f?a:o||d)?[]:g:q;c&&c(q,r,h,i);if(d){l=bk(r,n),d(l,[],h,i),j=l.length;while(j--)if(k=l[j])r[n[j]]=!(q[n[j]]=k)}if(f){j=a&&r.length;while(j--)if(k=r[j])f[m[j]]=!(g[m[j]]=k)}else r=bk(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):w.apply(g,r)})}function bm(a){var b,c,d,f=a.length,g=e.relative[a[0].type],h=g||e.relative[" "],i=g?1:0,j=bi(function(a){return a===b},h,!0),k=bi(function(a){return y.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i<f;i++)if(c=e.relative[a[i].type])m=[bi(bj(m),c)];else{c=e.filter[a[i].type].apply(null,a[i].matches);if(c[o]){d=++i;for(;d<f;d++)if(e.relative[a[d].type])break;return bl(i>1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i<d&&bm(a.slice(i,d)),d<f&&bm(a=a.slice(d)),d<f&&a.join(""))}m.push(c)}return bj(m)}function bn(a,b){var d=b.length>0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)bc(a,b[e],c,d);return c}function bp(a,b,c,d,f){var g,h,j,k,l,m=bh(a),n=m.length;if(!d&&m.length===1){h=m[0]=m[0].slice(0);if(h.length>2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;b<c;b++)if(this[b]===a)return b;return-1},z=function(a,b){return a[o]=b==null||b,a},A=function(){var a={},b=[];return z(function(c,d){return b.push(c)>e.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="<a name='"+o+"'></a><div name='"+o+"'></div>",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d<b;d+=2)a.push(d);return a}),odd:bf(function(a,b,c){for(var d=1;d<b;d+=2)a.push(d);return a}),lt:bf(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},j=s.compareDocumentPosition?function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,h=b.parentNode,i=g;if(g===h)return bg(a,b);if(!g)return-1;if(!h)return 1;while(i)e.unshift(i),i=i.parentNode;i=h;while(i)f.unshift(i),i=i.parentNode;c=e.length,d=f.length;for(var j=0;j<c&&j<d;j++)if(e[j]!==f[j])return bg(e[j],f[j]);return j===c?bg(a,f[j],-1):bg(e[j],b,1)},[0,0].sort(j),m=!k,bc.uniqueSort=function(a){var b,c=1;k=m,a.sort(j);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1);return a},bc.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},i=bc.compile=function(a,b){var c,d=[],e=[],f=D[o][a];if(!f){b||(b=bh(a)),c=b.length;while(c--)f=bm(b[c]),f[o]?d.push(f):e.push(f);f=D(a,bn(e,d))}return f},r.querySelectorAll&&function(){var a,b=bp,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[":focus"],f=[":active",":focus"],h=s.matchesSelector||s.mozMatchesSelector||s.webkitMatchesSelector||s.oMatchesSelector||s.msMatchesSelector;X(function(a){a.innerHTML="<select><option selected=''></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'/>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=!c.nodeType&&c[0]||c,c=c.ownerDocument||c,a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cT[c]=cT[c]||[],cT[c].unshift(b)},prefilter:function(a,b){b?cS.unshift(a):cS.push(a)}}),p.Tween=cZ,cZ.prototype={constructor:cZ,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cZ.propHooks[this.prop];return a&&a.get?a.get(this):cZ.propHooks._default.get(this)},run:function(a){var b,c=cZ.propHooks[this.prop];return this.options.duration?this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cZ.propHooks._default.set(this),this}},cZ.prototype.init.prototype=cZ.prototype,cZ.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cZ.propHooks.scrollTop=cZ.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(c$(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bZ).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cW(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cR.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:c$("show"),slideUp:c$("hide"),slideToggle:c$("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cZ.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cO&&(cO=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cO),cO=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c_=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j={top:0,left:0},k=this[0],l=k&&k.ownerDocument;if(!l)return;return(d=l.body)===k?p.offset.bodyOffset(k):(c=l.documentElement,p.contains(c,k)?(typeof k.getBoundingClientRect!="undefined"&&(j=k.getBoundingClientRect()),e=da(l),f=c.clientTop||d.clientTop||0,g=c.clientLeft||d.clientLeft||0,h=e.pageYOffset||c.scrollTop,i=e.pageXOffset||c.scrollLeft,{top:j.top+h-f,left:j.left+i-g}):j)},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); \ No newline at end of file diff --git a/career/js/jquery.min.js b/career/js/jquery.min.js new file mode 100644 index 0000000..764485c --- /dev/null +++ b/career/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,"display"),ia([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=W.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:"number"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=W.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Tb.pop()||r.expando+"_"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ub.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,"$1"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r}); diff --git a/career/lang/en/block_career.php b/career/lang/en/block_career.php new file mode 100644 index 0000000..cb34858 --- /dev/null +++ b/career/lang/en/block_career.php @@ -0,0 +1,18 @@ +<?php + $string['date_release'] = '{$a->month}.{$a->date}.{$a->year}'; + $string['career'] = 'Moodle Version'; + $string['career:addinstance'] = 'Ajouter un block Parcours'; + $string['career:myaddinstance'] = 'Ajouter un block Parcours sur ma page'; + $string['pluginname'] = 'Path block'; + $string['release'] = 'Release: '; + $string['title_plugin'] = 'Path'; + $string['titleadd_plugin'] = 'New Path'; + $string['titleaddname_plugin'] = 'Name'; + $string['titleadddesc_plugin'] = 'Description'; + $string['titleaddimg_plugin'] = 'Course picture'; + $string['titleaddelem_plugin'] = 'Course elements'; + $string['titleaddelemdesc_plugin'] = 'Drag and drop the elements of the course you want to add to this course'; + $string['heading_plugin'] = 'The path allow you to group some course resources in a particular sequence.'; + $string['add_path'] = 'Add a path'; + $string['any_carrer'] = 'No Path in this course'; +?> \ No newline at end of file diff --git a/career/lang/fr/block_career.php b/career/lang/fr/block_career.php new file mode 100644 index 0000000..dc95db7 --- /dev/null +++ b/career/lang/fr/block_career.php @@ -0,0 +1,17 @@ +<?php + $string['date_release'] = '{$a->month}.{$a->date}.{$a->year}'; + $string['career'] = 'Version Moodle'; + $string['career:addinstance'] = 'Ajouter un block Parcours'; + $string['career:myaddinstance'] = 'Ajouter un block Parcours sur ma page'; + $string['pluginname'] = 'Bloc Parcours'; + $string['title_plugin'] = 'Parcours'; + $string['titleadd_plugin'] = 'Ajouter un parcours'; + $string['titleaddname_plugin'] = 'Nom'; + $string['titleadddesc_plugin'] = 'Description'; + $string['titleaddimg_plugin'] = 'Image du parcours'; + $string['titleaddelem_plugin'] = 'Elements du parcours'; + $string['titleaddelemdesc_plugin'] = 'Glissez-déposez les élements du cours que vous souhaitez ajouter à ce parcours'; + $string['heading_plugin'] = 'Les parcours permettent de regrouper certaines ressources du cours dans un enchaînement particulier.'; + $string['add_path'] = 'Ajouter un parcours'; + $string['any_carrer'] = 'Pas de parcours'; +?> \ No newline at end of file diff --git a/career/styles.css b/career/styles.css new file mode 100644 index 0000000..aa8ed19 --- /dev/null +++ b/career/styles.css @@ -0,0 +1,260 @@ +.btn-career-block { + width: 100%; +} + +/*.left { + float: left; +}*/ + +/*.img_moodle_course { + max-width: 100%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + display: block; + margin: -1px; + min-height: 64px; + min-width: 64px; +}*/ + +/*.padding_column { + padding: 2rem; +}*/ + +/*.align_center { + display: flex; + justify-content: center; + align-items: center; +}*/ + +/*.button { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); +}*/ + +/*.img_center { + top: 1rem; + left: 1rem; +}*/ + +/*.img_moodle_list { + max-width: 100%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + display: block; + margin: -1px; + height: 32px; + width: 32px; + +}*/ + +.left_course_elements, .right_course_elements { + width: 33%; + float: left; + padding: 1%; + color: white; + text-align: center; + -webkit-box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5; + box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5; +} + +.left_course_elements { + background: #2d2d2d; +} + +.right_course_elements { + background: #009186; + +} + +/*.title { + font-size: 1.5rem; + color: white; + padding-bottom: 0.5rem +}*/ + +.middle_elements { + text-align: center; + width: 11%; + float: left; + padding: 1%; +} + +/*#outer-dropzone { + height: 140px; +}*/ + +/*#inner-dropzone { + height: 80px; +}*/ + +/*.dropzone { + background-color: #ccc; + border: dashed 4px transparent; + border-radius: 4px; + margin: 10px auto 30px; + padding: 10px; + width: 80%; + transition: background-color 0.3s; +}*/ + +/*.drop-active { + border-color: #aaa; +}*/ + +/*.drop-target { + background-color: #29e; + border-color: #fff; + border-style: solid; +}*/ + +/*.drag-drop { + display: inline-block; + min-width: 40px; + padding: 2em 0.5em; + + color: #fff; + background-color: #29e; + border: solid 2px #fff; + + -webkit-transform: translate(0px, 0px); + transform: translate(0px, 0px); + + transition: background-color 0.3s; +}*/ + +/*.drag-drop.can-drop { + color: #000; + background-color: #4e4; +}*/ + +/*.textarea:not([rows]) { + max-height: 600px; + min-height: 120px; +}*/ + +/*.textarea { + display: block; + max-width: 99%; + min-width: 99%; + padding: 0.625em; + resize: vertical; +}*/ + +/*.input, .textarea { + -moz-appearance: none; + -webkit-appearance: none; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: 1px solid transparent; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + border-left-color: transparent; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + background-color: white; + border-color: #dbdbdb; + color: #363636; + -webkit-box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); + margin: 0.5rem; + width: auto; + max-width: 90%; + +}*/ + +/*input[type='file'] { + margin-bottom: 1rem; + padding: 0rem; + background: none; + border: 0px; + box-shadow: none; +}*/ + +.subject-info-box-1, +.subject-info-box-2 { + float: left; + width: 100%; +} + +.subject-info-box-1 select, +.subject-info-box-2 select { + height: 200px; + padding: 0; +} + +.subject-info-box-1 select option, +.subject-info-box-2 select option { + padding: 4px 10px 4px 10px; +} + +.subject-info-box-1 select option:hover, +.subject-info-box-2 select option:hover { + background: #EEEEEE; +} + +/*.subject-info-arrows { + float: left; + width: 50%; + margin-left: 25%; +}*/ + +/*.subject-info-arrows input { + width: 70%; + margin-bottom: 5px; +}*/ + +.path-list-edit-link { + position: absolute; + top: 0; + right: 0; +} + +.iena-btn-career-arrow { + padding: 0.5rem; +} + +/*.iena-career-description { + background: #EEE; + color: #323232; + padding: 1rem; + clear: both; + margin-bottom: 0, 5rem; +}*/ + +/*.iena-carrer-path-descr { + border: 1px solid #1587bc; + border-radius: 0.15rem; + background-color: white; + color: #333; + padding: 1rem; +} */ + +/*.iena-carrer-path-descr a { + color: #1587bc; + margin-top: 1rem; + display: block; +}*/ + +/*.iena-career-description p, +.iena-carrer-path-descr p { + font-size: initial !important; +}*/ diff --git a/career/version.php b/career/version.php new file mode 100644 index 0000000..c932b00 --- /dev/null +++ b/career/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 details + * + * The block career plugin add career in moodle it is a different + * way to screen modules + * + * @package block_career + * @category block + * @copyright 2018 Softia/Université lorraine + * @author Vrignaud Camille / Kridagh Faouzi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + defined('MOODLE_INTERNAL') || die(); + + $plugin->version = 2018082901; + $plugin->requires = 2014051200; + $plugin->component = 'block_career'; + $plugin->release = 'v1.1'; + $plugin->maturity = MATURITY_STABLE; \ No newline at end of file diff --git a/career/view/view_career_list.php b/career/view/view_career_list.php new file mode 100644 index 0000000..1e04a0c --- /dev/null +++ b/career/view/view_career_list.php @@ -0,0 +1,55 @@ +<?php + + + class view_career_list + { + + /** + * @return string + */ + public function get_content() + { + global $DB, $CFG; + + $content = "<h2>" . get_string('title_plugin', 'block_career') . "</h2>"; + $content .= "<div class='alert alert-info'>" . get_string('heading_plugin', 'block_career') . "</div>"; + + $request = $DB->get_records_sql('SELECT * FROM {block_career} WHERE course = ?', array($_GET["course"])); + + // $image = ""; + + foreach ($request as $value) { + + // if (file_get_contents($value->image) != null) { + // $image = "<img src='$value->image' class='img_moodle_course'/>"; + // } + + $content .= "<div class='card card-block mb-3'> + <div class='card-body'> + <h2 class='card-title'>$value->name</h2> + <p class='card-text'>$value->description</p> + <a href='$CFG->wwwroot/blocks/career/career_setting.php?course=" . $_GET["course"] . "&id=$value->id' class='btn btn-primary btn-sm path-list-edit-link'>Modifier</a> + </div> + </div>"; + + // $content .= "<div class='card card_block'> + // <div class='row'> + // <div class='col-lg-1 col-md-1 padding_column align_center img_center' >$image</div> + // <div class='col-lg-10 col-md-10 padding_column'><h3>$value->name</h3>$value->description</div> + // <div class='col-lg-1 col-md-1 padding_column'><a style='color:black' href='$CFG->wwwroot/blocks/career/career_setting.php?course=" . $_GET["course"] . "&id=$value->id'><i class=\"fa fa-cog fa-2x\"></a></i></div> + // </div> + // </div>"; + } + + if (empty($request)) { + $content .= "<p>" . get_string('any_carrer', 'block_career') . "</p>"; + } + // Button for adding course to the list + $content .= "<a href='$CFG->wwwroot/blocks/career/career_setting.php?course=" . $_GET["course"] . "' class='btn btn-primary'>" . get_string('add_path', 'block_career') . "</a>"; + + + return $content; + + } + + } \ No newline at end of file diff --git a/career/view/view_career_setting.php b/career/view/view_career_setting.php new file mode 100644 index 0000000..26d06de --- /dev/null +++ b/career/view/view_career_setting.php @@ -0,0 +1,196 @@ +<?php + +/** + * block_career + * + * + * @package block_career + * @category block + * @copyright 2018 Softia/Université lorraine + * @author vrignaud camille/ faouzi / Thomas Fradet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class view_career_setting extends moodleform +{ + public function definition() + { + // TODO: Implement definition() method. + global $CFG; + + $mform = $this->_form; // Don't forget the underscore! + + //Default value + } + + public function get_content() + { + global $DB, $CFG; + + $mform = $this->_form; + $careerId = optional_param('id', NULL, PARAM_INT); + $course = required_param('course', PARAM_INT); + + $name = ""; + $description = ""; + $ressourcesId = ""; + $contentButton = "Ajouter un parcours"; + // $imagePath = ""; + + if (isset($careerId) && !empty($careerId)) + { + $requete = $DB->get_record_sql('SELECT * FROM {block_career} WHERE id = ?', array($careerId)); + $name = $requete->name; + $description = $requete->description; + $ressourcesId = explode(",", $requete->ressources); + $contentButton = "Modifier le parcours"; + // $imagePath = $requete->image; + + } + + + $mform->addElement('text','careerName',get_string('titleaddname_plugin', 'block_career')); + $mform->addRule('careerName', get_string('error'), 'required', null, null, false, false); + $mform->setDefault('careerName',$name); + + $mform->addElement('editor', 'descriptionName', get_string('titleadddesc_plugin', 'block_career')); + $mform->setType('descriptionName', PARAM_RAW); + $mform->addRule('descriptionName', get_string('error'), 'required', null, null, false, false); + $mform->setDefault('descriptionName',array('text'=>$description)); + + //FilePicker IMAGE + //$mform->addElement('filepicker', 'imageName', get_string('titleaddimg_plugin', 'block_career'), null); + + + // $content = "<h1>" . get_string('title_plugin', 'block_career') . "</h1>"; + $content .= "<div class='alert alert-info'>" . get_string('heading_plugin', 'block_career') . "</div>"; + + $temp = $mform->toHtml(); + + + $temp = substr($temp,(strpos($temp,'>')+1)); + $temp = substr($temp,0, -7); + $content .= '<div class="row"><form class="col-12" action="career_setting.php?course=' . $course . '" method="post" enctype="multipart/form-data">'; + + $content .= $temp; + + // $content .= ' <section class="section"><h3>' . get_string('titleaddimg_plugin', 'block_career') . '</h3> + // <input type="file" class="input" name="imageName" accept="image/*" /></section>'; + + + $content .= ' <section class="section row"><div class="col-12"><h2>' . get_string('titleaddelem_plugin', 'block_career') . '</h2> + <div class="alert alert-info">' . get_string('titleaddelemdesc_plugin', 'block_career') . '</div> + <div class="left_course_elements"> + <div class="title">Cours</div> + <div class="subject-info-box-1"> + <select multiple="multiple" id="lstBox1" class="form-control">'; + + $sections = block_career_section::get_sections_by_id_course($course); + + // var_dump($sections); + + foreach ($sections as $section) + { + $section->ressources = block_career_ressource::get_ressources_by_id_section($section->id); + } + + foreach ($sections as $section) + { + $content .= '<optgroup label="'.$section->name.'" value="'.$section->id.'">'; + + foreach ($section->ressources as $ressource) + { + $content .= '<option label="'.$ressource->name.'" value ="'.$ressource->id.'" name="'.$ressource->id.'">'.$ressource->name.'</option>'; + } + $content .= '</optgroup>'; + } + + $content .='</select></div></div>'; + + $content .= '<div class="middle_elements"> + <div class="title">Actions</div> + <div class="subject-info-arrows text-center"> + <input type="button" id="btnAllRight" value=">>" class="btn btn-default iena-btn-career-arrow" /><br /> + <input type="button" id="btnRight" value=">" class="btn btn-default iena-btn-career-arrow" /><br /> + <input type="button" id="btnLeft" value="<" class="btn btn-default iena-btn-career-arrow" /><br /> + <input type="button" id="btnAllLeft" value="<<" class="btn btn-default iena-btn-career-arrow" /> + </div> + </div>'; + + $content .= '<div class="right_course_elements"> + <div class="title">Parcours</div> + <div class="subject-info-box-2"> + <select multiple="multiple" id="lstBox2" name="ressource[]" class="form-control" required>'; + + + foreach ($ressourcesId as $value) + { + $res = new block_career_ressource(); + $res->get_ressource_by_id($value); + + if ($careerId != 0) + $content .= '<option label="'.$res->name.'" value ="'.$res->id.'">'; + } + + + $content .= '</select> + </div> + <div class="clearfix"></div> + </div> + </div> + </section>'; + + $content .= '<script> + function selectAll(e) + { + // e.preventDefault(); + selectBox = document.getElementById("lstBox2"); + + var checkValue = 0; + + for (var i = 0; i < selectBox.options.length; i++) + { + selectBox.options[i].selected = true; + } + + $("#lstBox2 :selected").map(function(i, el) { + + if (checkValue == $(el).val()) + $(el).remove(); + + checkValue = $(el).val(); + }); + } + // console.log() + </script>'; + + if ($careerId != 0) { + $content .= '<input type="hidden" name="imagePath" value="'.$imagePath.'">'; + } + + $content .= ' + <div class="row mt-3"> + <div class="col"> + <input type="hidden" name="careerId" value="'.$careerId.'"> + <a href=' . $CFG->wwwroot . "/blocks/career/career_list.php?course=" . $course . ' class="btn btn-secondary">Annuler</a> '; + + + + if ($careerId != 0) { + $content .= "<a href='$CFG->wwwroot/blocks/career/career_setting.php?course=$course&delete=1&id=$careerId' class='btn btn-danger'>Supprimer</a> "; + } + + $content .= ' + <button type="submit" onclick="selectAll(event);" class="btn btn-primary">'.$contentButton.'</button> + </div> + </div> + '; + + + $content .= '</div></form>'; + + + return $content; + } + + } \ No newline at end of file diff --git a/career/view/view_career_unit.php b/career/view/view_career_unit.php new file mode 100644 index 0000000..a67f079 --- /dev/null +++ b/career/view/view_career_unit.php @@ -0,0 +1,106 @@ +<?php + +$careerId = required_param("career", PARAM_INT); +global $DB; +$requete = $DB->get_record_sql('SELECT * FROM {block_career} WHERE id = ?', array($careerId)); + +$percent = 70; +$nb_pers = 5; +$titre = $requete->name; +$presence = "En présence"; +$date = "24 nov"; +$intro = $requete->description; +$img = ''; +$titre_module = "Introduction"; + +$elements = $requete->ressources; +$elements = explode(',', $elements); +$sections = array(); +$ressources = array(); +$i = 0; +foreach ($elements as $value) { + $ressource = new block_career_ressource(); + $ressource->get_ressource_by_id($value); + $sections[$i] = $ressource->section; + $ressources[$i] = $ressource; + $i++; +} +//var_dump($sections); +//Supprime les doublons +for($i = 0; $i < count($sections);$i++) +{ + $temp = $i; + $temp++; + + if ($temp != count($sections)) + { + if ($sections[$i]->id == $sections[$temp]->id) + { + unset($sections[$i]); + } + } +} + +//Met dans l'orde +$keys = array(); +$i = 0; +foreach ($sections as $value){ + $keys[$i] = $value->orde; + $i++; +} +$sections = array_combine($keys,$sections); +ksort($sections); + + +?> + + +<section class="section"> + <h2 class="display-3"><?=$titre;?></h2> + <div class="iena-carrer-path-descr wrapper"> + <div class="small"> + <?= $intro ;?> + </div> + <a href="#">Voir la description complète</a> + </div> + <?php foreach ($sections as $section) : ?> + <div style="margin-bottom: 0rem; margin-top: 1rem;"> + <div class="card card_block"> + <div class="heading-iena set_height" style="background-color: #009186 !important;"> + <div class="titre_section set_height"> + <h3><?php echo $section->name; ?></h3> + </div> + </div> + </div> + <div class="iena-career-description wrapper"> + <div class="small"> + <p><?= $section->intro ;?></p> + </div> + <a href="#">Voir la description complète</a> + + </div> + <div class="elements"> + <div class="list-group"> + <?php foreach ($ressources as $value) : ?> + <?php if($value->section->id == $section->id) : ?> + <div class="row" style="padding-bottom: 0.5rem;"> + <div class="col-md-12 col-sm-12 col-lg-12"> + <a href="<?php echo "$value->link&career=$careerId" ?>" class="list-group-item list-group-item-action flex-column align-items-start"> + <div class="d-flex w-100 justify-content-between"> + <h5 class="mb-1"><img class="" alt="" src="<?php echo $CFG->wwwroot ?>/theme/image.php/boost/<?php echo $value->type ?>/1/icon>"> + <?php echo $value->name;?></h5></div> + + <!--<div style="max-height:100px;overflow-y:auto;"><p class="mb-1"><?php echo $value->descrition;?></p></div>--></a> + </div> + </div> + <?php endif;?> + <?php endforeach;?> + </div> + </div></div> + <?php endforeach;?> + <!-- </ul> --> + <!-- </div> --> + + + + </section> diff --git a/classes 14.04.12/external.php b/classes 14.04.12/external.php new file mode 100644 index 0000000..95de3b8 --- /dev/null +++ b/classes 14.04.12/external.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/>. + +/** + * Blocks external API + * + * @package core_block + * @category external + * @copyright 2017 Juan Leyva <juan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.3 + */ + +defined('MOODLE_INTERNAL') || die; + +require_once("$CFG->libdir/externallib.php"); + +/** + * Blocks external functions + * + * @package core_block + * @category external + * @copyright 2015 Juan Leyva <juan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.3 + */ +class core_block_external extends external_api { + + /** + * Returns description of get_course_blocks parameters. + * + * @return external_function_parameters + * @since Moodle 3.3 + */ + public static function get_course_blocks_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'course id') + ) + ); + } + + /** + * Returns blocks information for a course. + * + * @param int $courseid The course id + * @return array Blocks list and possible warnings + * @throws moodle_exception + * @since Moodle 3.3 + */ + public static function get_course_blocks($courseid) { + global $OUTPUT, $PAGE; + + $warnings = array(); + $params = self::validate_parameters(self::get_course_blocks_parameters(), ['courseid' => $courseid]); + + $course = get_course($params['courseid']); + $context = context_course::instance($course->id); + self::validate_context($context); + + // Specific layout for frontpage course. + if ($course->id == SITEID) { + $PAGE->set_pagelayout('frontpage'); + $PAGE->set_pagetype('site-index'); + } else { + $PAGE->set_pagelayout('course'); + // Ensure course format is set (view course/view.php). + $course->format = course_get_format($course)->get_format(); + $PAGE->set_pagetype('course-view-' . $course->format); + } + + // Load the block instances for all the regions. + $PAGE->blocks->load_blocks(); + $PAGE->blocks->create_all_block_instances(); + + $finalblocks = array(); + $blocks = $PAGE->blocks->get_content_for_all_regions($OUTPUT); + foreach ($blocks as $region => $regionblocks) { + foreach ($regionblocks as $bc) { + $finalblocks[] = [ + 'instanceid' => $bc->blockinstanceid, + 'name' => $bc->attributes['data-block'], + 'region' => $region, + 'positionid' => $bc->blockpositionid, + 'collapsible' => (bool) $bc->collapsible, + 'dockable' => (bool) $bc->dockable, + ]; + } + } + + return array( + 'blocks' => $finalblocks, + 'warnings' => $warnings + ); + } + + /** + * Returns description of get_course_blocks result values. + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function get_course_blocks_returns() { + + return new external_single_structure( + array( + 'blocks' => new external_multiple_structure( + new external_single_structure( + array( + 'instanceid' => new external_value(PARAM_INT, 'Block instance id.'), + 'name' => new external_value(PARAM_PLUGIN, 'Block name.'), + 'region' => new external_value(PARAM_ALPHANUMEXT, 'Block region.'), + 'positionid' => new external_value(PARAM_INT, 'Position id.'), + 'collapsible' => new external_value(PARAM_BOOL, 'Whether the block is collapsible.'), + 'dockable' => new external_value(PARAM_BOOL, 'hether the block is dockable.'), + ), 'Block information.' + ), 'List of blocks in the course.' + ), + 'warnings' => new external_warnings(), + ) + ); + } + +} diff --git a/classes 14.04.12/privacy/provider.php b/classes 14.04.12/privacy/provider.php new file mode 100644 index 0000000..cbee644 --- /dev/null +++ b/classes 14.04.12/privacy/provider.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/>. + +/** + * Data provider. + * + * @package core_block + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart <fred@branchup.tech> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_block\privacy; +defined('MOODLE_INTERNAL') || die(); + +use context; +use context_block; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +/** + * Data provider class. + * + * @package core_block + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart <fred@branchup.tech> + * @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\subsystem\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * 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) : collection { + $collection->add_user_preference('blockIDhidden', 'privacy:metadata:userpref:hiddenblock'); + $collection->add_user_preference('docked_block_instance_ID', 'privacy:metadata:userpref:dockedinstance'); + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { + global $DB; + $contextlist = new \core_privacy\local\request\contextlist(); + + // Fetch the block instance IDs. + $likehidden = $DB->sql_like('name', ':hidden', false, false); + $likedocked = $DB->sql_like('name', ':docked', false, false); + $sql = "userid = :userid AND ($likehidden OR $likedocked)"; + $params = [ + 'userid' => $userid, + 'hidden' => 'block%hidden', + 'docked' => 'docked_block_instance_%', + ]; + $prefs = $DB->get_fieldset_select('user_preferences', 'name', $sql, $params); + + $instanceids = array_unique(array_map(function($prefname) { + if (preg_match('/^block(\d+)hidden$/', $prefname, $matches)) { + return $matches[1]; + } else if (preg_match('/^docked_block_instance_(\d+)$/', $prefname, $matches)) { + return $matches[1]; + } + return 0; + }, $prefs)); + + // Find the context of the instances. + if (!empty($instanceids)) { + list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED); + $sql = " + SELECT ctx.id + FROM {context} ctx + WHERE ctx.instanceid $insql + AND ctx.contextlevel = :blocklevel"; + $params = array_merge($inparams, ['blocklevel' => CONTEXT_BLOCK]); + $contextlist->add_from_sql($sql, $params); + } + + return $contextlist; + } + + /** + * 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; + $userid = $contextlist->get_user()->id; + + // Extract the block instance IDs. + $instanceids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_BLOCK) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + if (empty($instanceids)) { + return; + } + + // Query the blocks and their preferences. + list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED); + $hiddenkey = $DB->sql_concat("'block'", 'bi.id', "'hidden'"); + $dockedkey = $DB->sql_concat("'docked_block_instance_'", 'bi.id'); + $sql = " + SELECT bi.id, h.value AS prefhidden, d.value AS prefdocked + FROM {block_instances} bi + LEFT JOIN {user_preferences} h + ON h.userid = :userid1 + AND h.name = $hiddenkey + LEFT JOIN {user_preferences} d + ON d.userid = :userid2 + AND d.name = $dockedkey + WHERE bi.id $insql + AND (h.id IS NOT NULL + OR d.id IS NOT NULL)"; + $params = array_merge($inparams, [ + 'userid1' => $userid, + 'userid2' => $userid, + ]); + + // Export all the things. + $dockedstr = get_string('privacy:request:blockisdocked', 'core_block'); + $hiddenstr = get_string('privacy:request:blockishidden', 'core_block'); + $recordset = $DB->get_recordset_sql($sql, $params); + foreach ($recordset as $record) { + $context = context_block::instance($record->id); + if ($record->prefdocked !== null) { + writer::with_context($context)->export_user_preference( + 'core_block', + 'block_is_docked', + transform::yesno($record->prefdocked), + $dockedstr + ); + } + if ($record->prefhidden !== null) { + writer::with_context($context)->export_user_preference( + 'core_block', + 'block_is_hidden', + transform::yesno($record->prefhidden), + $hiddenstr + ); + } + } + $recordset->close(); + } + + /** + * Export 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) { + // Our preferences aren't site-wide so they are exported in export_user_data. + } + + /** + * 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->contextlevel != CONTEXT_BLOCK) { + return; + } + + // Delete the user preferences. + $instanceid = $context->instanceid; + $DB->delete_records_list('user_preferences', 'name', [ + "block{$instanceid}hidden", + "docked_block_instance_{$instanceid}" + ]); + } + + /** + * 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 = $contextlist->get_user()->id; + $prefnames = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_BLOCK) { + $carry[] = "block{$context->instanceid}hidden"; + $carry[] = "docked_block_instance_{$context->instanceid}"; + } + return $carry; + }, []); + + if (empty($prefnames)) { + return; + } + + list($insql, $inparams) = $DB->get_in_or_equal($prefnames, SQL_PARAMS_NAMED); + $sql = "userid = :userid AND name $insql"; + $params = array_merge($inparams, ['userid' => $userid]); + $DB->delete_records_select('user_preferences', $sql, $params); + } + +} diff --git a/comments/block_comments.php b/comments/block_comments.php new file mode 100644 index 0000000..b36facc --- /dev/null +++ b/comments/block_comments.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/>. + +/** + * The comments block + * + * @package block_comments + * @copyright 2009 Dongsheng Cai <dongsheng@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Obviously required +require_once($CFG->dirroot . '/comment/lib.php'); + +class block_comments extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_comments'); + } + + function specialization() { + // require js for commenting + comment::init(); + } + function applicable_formats() { + return array('all' => true); + } + + function instance_allow_multiple() { + return false; + } + + function get_content() { + global $CFG, $PAGE; + if ($this->content !== NULL) { + return $this->content; + } + if (!$CFG->usecomments) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('disabledcomments'); + } + return $this->content; + } + $this->content = new stdClass(); + $this->content->footer = ''; + $this->content->text = ''; + if (empty($this->instance)) { + return $this->content; + } + list($context, $course, $cm) = get_context_info_array($PAGE->context->id); + + $args = new stdClass; + $args->context = $PAGE->context; + $args->course = $course; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + $comment->set_view_permission(true); + $comment->set_fullwidth(); + + $this->content = new stdClass(); + $this->content->text = $comment->output(true); + $this->content->footer = ''; + return $this->content; + } +} diff --git a/comments/classes/event/comment_created.php b/comments/classes/event/comment_created.php new file mode 100644 index 0000000..5864f83 --- /dev/null +++ b/comments/classes/event/comment_created.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/>. + +/** + * block_comments comment created event. + * + * @package block_comments + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_comments\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * block_comments comment created event. + * + * @package block_comments + * @since Moodle 2.7 + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_created extends \core\event\comment_created { + // No need to override any method. +} diff --git a/comments/classes/event/comment_deleted.php b/comments/classes/event/comment_deleted.php new file mode 100644 index 0000000..6e1214d --- /dev/null +++ b/comments/classes/event/comment_deleted.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/>. + +/** + * block_comments comment deleted event. + * + * @package block_comments + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_comments\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * block_comments comment deleted event. + * + * @package block_comments + * @since Moodle 2.7 + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_deleted extends \core\event\comment_deleted { + // No need to override any method. +} diff --git a/comments/classes/privacy/provider.php b/comments/classes/privacy/provider.php new file mode 100644 index 0000000..d758293 --- /dev/null +++ b/comments/classes/privacy/provider.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/>. + +/** + * Privacy Subsystem implementation for block_comments. + * + * @package block_comments + * @category privacy + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_comments\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; + +/** + * Privacy Subsystem implementation for block_comments. + * + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The block_comments block stores user provided data. + \core_privacy\local\metadata\provider, + + // The block_comments block provides data directly to core. + \core_privacy\local\request\plugin\provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection) : collection { + return $collection->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comment'); + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid + * @return contextlist + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new contextlist(); + + $sql = "SELECT contextid + FROM {comments} + WHERE component = :component + AND userid = :userid"; + $params = [ + 'component' => 'block_comments', + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist + */ + public static function export_user_data(approved_contextlist $contextlist) { + $contexts = $contextlist->get_contexts(); + foreach ($contexts as $context) { + \core_comment\privacy\provider::export_comments( + $context, + 'block_comments', + 'page_comments', + 0, + [] + ); + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context + */ + public static function delete_data_for_all_users_in_context(\context $context) { + \core_comment\privacy\provider::delete_comments_for_all_users($context, 'block_comments'); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'block_comments'); + } +} diff --git a/comments/db/access.php b/comments/db/access.php new file mode 100644 index 0000000..1122993 --- /dev/null +++ b/comments/db/access.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/>. + +/** + * Comments block caps. + * + * @package block_comments + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/comments:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/comments: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/comments/lang/en/block_comments.php b/comments/lang/en/block_comments.php new file mode 100644 index 0000000..116f10e --- /dev/null +++ b/comments/lang/en/block_comments.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/>. + +/** + * Strings for component 'block_comments', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_comments + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['comments:myaddinstance'] = 'Add a new comments block to Dashboard'; +$string['comments:addinstance'] = 'Add a new comments block'; +$string['pluginname'] = 'Comments'; +$string['privacy:metadata:core_comment'] = 'A record of comments added.'; diff --git a/comments/lib.php b/comments/lib.php new file mode 100644 index 0000000..454d1cd --- /dev/null +++ b/comments/lib.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/>. + +/** + * The comments block helper functions and callbacks + * + * @package block_comments + * @copyright 2011 Dongsheng Cai <dongsheng@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Validate comment parameter before perform other comments actions + * + * @package block_comments + * @category comment + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function block_comments_comment_validate($comment_param) { + if ($comment_param->commentarea != 'page_comments') { + throw new comment_exception('invalidcommentarea'); + } + if ($comment_param->itemid != 0) { + throw new comment_exception('invalidcommentitemid'); + } + return true; +} + +/** + * Running addtional permission check on plugins + * + * @package block_comments + * @category comment + * + * @param stdClass $args + * @return array + */ +function block_comments_comment_permissions($args) { + return array('post'=>true, 'view'=>true); +} + +/** + * Validate comment data before displaying comments + * + * @package block_comments + * @category comment + * + * @param stdClass $comment + * @param stdClass $args + * @return boolean + */ +function block_comments_comment_display($comments, $args) { + if ($args->commentarea != 'page_comments') { + throw new comment_exception('invalidcommentarea'); + } + if ($args->itemid != 0) { + throw new comment_exception('invalidcommentitemid'); + } + return $comments; +} diff --git a/comments/tests/behat/add_comment.feature b/comments/tests/behat/add_comment.feature new file mode 100644 index 0000000..02f73f4 --- /dev/null +++ b/comments/tests/behat/add_comment.feature @@ -0,0 +1,98 @@ +@block @block_comments +Feature: Add a comment to the comments block + In order to comment on a conversation or a topic + As a user + In need to add comments to courses + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Frist | teacher1@example.com | + | student1 | Student | First | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Comments" block + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + + @javascript + Scenario: Add a comment with Javascript enabled + When I add "I'm a comment from student1" comment to comments block + Then I should see "I'm a comment from student1" + + Scenario: Add a comment with Javascript disabled + When I follow "Show comments" + And I add "I'm a comment from student1" comment to comments block + Then I should see "I'm a comment from student1" + + @javascript + Scenario: Test comment block pagination + When I add "Super test comment 01" comment to comments block + And I add "Super test comment 02" comment to comments block + And I add "Super test comment 03" comment to comments block + And I add "Super test comment 04" comment to comments block + And I add "Super test comment 05" comment to comments block + And I add "Super test comment 06" comment to comments block + And I add "Super test comment 07" comment to comments block + And I add "Super test comment 08" comment to comments block + And I add "Super test comment 09" comment to comments block + And I add "Super test comment 10" comment to comments block + And I add "Super test comment 11" comment to comments block + And I add "Super test comment 12" comment to comments block + And I add "Super test comment 13" comment to comments block + And I add "Super test comment 14" comment to comments block + And I add "Super test comment 15" comment to comments block + And I add "Super test comment 16" comment to comments block + And I add "Super test comment 17" comment to comments block + And I add "Super test comment 18" comment to comments block + And I add "Super test comment 19" comment to comments block + And I add "Super test comment 20" comment to comments block + And I add "Super test comment 21" comment to comments block + And I add "Super test comment 22" comment to comments block + And I add "Super test comment 23" comment to comments block + And I add "Super test comment 24" comment to comments block + And I add "Super test comment 25" comment to comments block + And I add "Super test comment 26" comment to comments block + And I add "Super test comment 27" comment to comments block + And I add "Super test comment 28" comment to comments block + And I add "Super test comment 29" comment to comments block + And I add "Super test comment 30" comment to comments block + And I add "Super test comment 31" comment to comments block + Then I should see "Super test comment 01" + And I should see "Super test comment 31" + And I am on "Course 1" course homepage + And I should not see "Super test comment 01" + And I should not see "Super test comment 02" + And I should not see "Super test comment 16" + And I should see "Super test comment 17" + And I should see "Super test comment 31" + And I should see "1" in the ".block_comments .comment-paging" "css_element" + And I should see "2" in the ".block_comments .comment-paging" "css_element" + And I should see "3" in the ".block_comments .comment-paging" "css_element" + And I should not see "4" in the ".block_comments .comment-paging" "css_element" + And I click on "2" "link" in the ".block_comments .comment-paging" "css_element" + And I should not see "Super test comment 01" + And I should see "Super test comment 02" + And I should see "Super test comment 16" + And I should not see "Super test comment 17" + And I should not see "Super test comment 31" + And I click on "3" "link" in the ".block_comments .comment-paging" "css_element" + And I should see "Super test comment 01" + And I should not see "Super test comment 02" + And I should not see "Super test comment 16" + And I should not see "Super test comment 17" + And I should not see "Super test comment 31" + And I click on "1" "link" in the ".block_comments .comment-paging" "css_element" + And I should not see "Super test comment 01" + And I should not see "Super test comment 02" + And I should not see "Super test comment 16" + And I should see "Super test comment 17" + And I should see "Super test comment 31" diff --git a/comments/tests/behat/behat_block_comments.php b/comments/tests/behat/behat_block_comments.php new file mode 100644 index 0000000..26f7495 --- /dev/null +++ b/comments/tests/behat/behat_block_comments.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/>. + +/** + * Commenting system steps definitions. + * + * @package block_comments + * @category test + * @copyright 2013 David Monllaó + * @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\Mink\Exception\ElementNotFoundException as ElementNotFoundException, + Behat\Mink\Exception\ExpectationException as ExpectationException; + +/** + * Steps definitions to deal with the commenting system + * + * @package block_comments + * @category test + * @copyright 2013 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_comments extends behat_base { + + /** + * Adds the specified option to the comments block of the current page. + * + * This method can be adapted in future to add other comments considering + * that there could be more than one comment textarea per page. + * + * Only 1 comments block instance is allowed per page, if this changes this + * steps definitions should be adapted. + * + * @Given /^I add "(?P<comment_text_string>(?:[^"]|\\")*)" comment to comments block$/ + * @throws ElementNotFoundException + * @param string $comment + */ + public function i_add_comment_to_comments_block($comment) { + + // Getting the textarea and setting the provided value. + $exception = new ElementNotFoundException($this->getSession(), 'Comments block '); + + // The whole DOM structure changes depending on JS enabled/disabled. + if ($this->running_javascript()) { + $commentstextarea = $this->find('css', '.comment-area textarea', $exception); + $commentstextarea->setValue($comment); + + $this->find_link(get_string('savecomment'))->click(); + // Delay after clicking so that additional comments will have unique time stamps. + // We delay 1 second which is all we need. + $this->getSession()->wait(1000); + + } else { + + $commentstextarea = $this->find('css', '.block_comments form textarea', $exception); + $commentstextarea->setValue($comment); + + // Comments submit button + $submit = $this->find('css', '.block_comments form input[type=submit]'); + $submit->press(); + } + } + + /** + * Deletes the specified comment from the current page's comments block. + * + * @Given /^I delete "(?P<comment_text_string>(?:[^"]|\\")*)" comment from comments block$/ + * @throws ElementNotFoundException + * @throws ExpectationException + * @param string $comment + */ + public function i_delete_comment_from_comments_block($comment) { + + $exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment '); + + // Using xpath liternal to avoid possible problems with comments containing quotes. + $commentliteral = behat_context_helper::escape($comment); + + $commentxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_comments ')]" . + "/descendant::div[@class='comment-message'][contains(., $commentliteral)]"; + $commentnode = $this->find('xpath', $commentxpath, $exception); + + // Click on delete icon. + $this->execute('behat_general::i_click_on_in_the', + array("Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element") + ); + + // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case. + $this->getSession()->wait(4 * 1000); + } + +} diff --git a/comments/tests/behat/block_comment_activity.feature b/comments/tests/behat/block_comment_activity.feature new file mode 100644 index 0000000..4aa94bd --- /dev/null +++ b/comments/tests/behat/block_comment_activity.feature @@ -0,0 +1,33 @@ +@block @block_comments +Feature: Enable Block comments on an activity page and view comments + In order to enable the comments block on an activity page + As a teacher + I can add the comments block to an activity page + + Scenario: Add the comments block on an activity page and add comments + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Frist | teacher1@example.com | + | student1 | Student | First | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | course | idnumber | name | intro | + | page | C1 | page1 | Test page name | Test page description | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + And I add the "Comments" block + And I follow "Show comments" + And I add "I'm a comment from the teacher" comment to comments block + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test page name" + And I follow "Show comments" + Then I should see "I'm a comment from the teacher" diff --git a/comments/tests/behat/block_comment_course.feature b/comments/tests/behat/block_comment_course.feature new file mode 100644 index 0000000..3589da4 --- /dev/null +++ b/comments/tests/behat/block_comment_course.feature @@ -0,0 +1,28 @@ +@block @block_comments +Feature: Enable Block comments on a course page and view comments + In order to enable the comments block on a course page + As a teacher + I can add the comments block to the course page + + Scenario: Add the comments block on the course page and add comments + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Frist | teacher1@example.com | + | student1 | Student | First | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Comments" block + And I follow "Show comments" + And I add "I'm a comment from the teacher" comment to comments block + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Show comments" + Then I should see "I'm a comment from the teacher" diff --git a/comments/tests/behat/block_comment_dashboard.feature b/comments/tests/behat/block_comment_dashboard.feature new file mode 100644 index 0000000..66e49c6 --- /dev/null +++ b/comments/tests/behat/block_comment_dashboard.feature @@ -0,0 +1,29 @@ +@block @block_comments +Feature: Enable Block comments on the dashboard and view comments + In order to enable the comments block on a the dashboard + As a teacher + I can add the comments block to my dashboard + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | Frist | teacher1@example.com | + + Scenario: Add the comments block on the dashboard and add comments with Javascript disabled + When I log in as "teacher1" + And I press "Customise this page" + And I add the "Comments" block + And I follow "Show comments" + And I add "I'm a comment from the teacher" comment to comments block + Then I should see "I'm a comment from the teacher" + + @javascript + Scenario: Add the comments block on the dashboard and add comments with Javascript enabled + When I log in as "teacher1" + And I press "Customise this page" + And I add the "Comments" block + And I add "I'm a comment from the teacher" comment to comments block + Then I should see "I'm a comment from the teacher" diff --git a/comments/tests/behat/block_comment_frontpage.feature b/comments/tests/behat/block_comment_frontpage.feature new file mode 100644 index 0000000..5d949ab --- /dev/null +++ b/comments/tests/behat/block_comment_frontpage.feature @@ -0,0 +1,21 @@ +@block @block_comments +Feature: Enable Block comments on the frontpage and view comments + In order to enable the comments block on the frontpage + As a admin + I can add the comments block to the frontpage + + Scenario: Add the comments block on the frontpage and add comments + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Comments" block + And I follow "Show comments" + And I add "I'm a comment from admin" comment to comments block + And I log out + When I log in as "teacher1" + And I am on site homepage + And I follow "Show comments" + Then I should see "I'm a comment from admin" diff --git a/comments/tests/behat/delete_comment.feature b/comments/tests/behat/delete_comment.feature new file mode 100644 index 0000000..f8939f4 --- /dev/null +++ b/comments/tests/behat/delete_comment.feature @@ -0,0 +1,34 @@ +@block @block_comments +Feature: Delete comment block messages + In order to refine comment block's contents + As a teacher + In need to delete comments from courses + + @javascript + Scenario: Delete comments with Javascript enabled + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | First | teacher1@example.com | + | student1 | Student | First | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Comments" block + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I add "Comment from student1" comment to comments block + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I add "Comment from teacher1" comment to comments block + When I delete "Comment from student1" comment from comments block + Then I should not see "Comment from student1" + And I delete "Comment from teacher1" comment from comments block + And I should not see "Comment from teacher1" diff --git a/comments/tests/events_test.php b/comments/tests/events_test.php new file mode 100644 index 0000000..1e7aae4 --- /dev/null +++ b/comments/tests/events_test.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/>. + +/** + * Events tests. + * + * @package block_comments + * @category test + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Events tests class. + * + * @package block_comments + * @category test + * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_comments_events_testcase extends advanced_testcase { + /** @var stdClass Keeps course object */ + private $course; + + /** @var stdClass Keeps wiki object */ + private $wiki; + + /** + * Setup test data. + */ + public function setUp() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create course and wiki. + $this->course = $this->getDataGenerator()->create_course(); + $this->wiki = $this->getDataGenerator()->create_module('wiki', array('course' => $this->course->id)); + } + + /** + * Test comment_created event. + */ + public function test_comment_created() { + global $CFG; + + require_once($CFG->dirroot . '/comment/lib.php'); + + // Comment on course page. + $context = context_course::instance($this->course->id); + $args = new stdClass; + $args->context = $context; + $args->course = $this->course; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $comment->add('New comment'); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\block_comments\event\comment_created', $event); + $this->assertEquals($context, $event->get_context()); + $url = new moodle_url('/course/view.php', array('id' => $this->course->id)); + $this->assertEquals($url, $event->get_url()); + + // Comments when block is on module (wiki) page. + $context = context_module::instance($this->wiki->cmid); + $args = new stdClass; + $args->context = $context; + $args->course = $this->course; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $comment->add('New comment 1'); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\block_comments\event\comment_created', $event); + $this->assertEquals($context, $event->get_context()); + $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid)); + $this->assertEquals($url, $event->get_url()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test comment_deleted event. + */ + public function test_comment_deleted() { + global $CFG; + + require_once($CFG->dirroot . '/comment/lib.php'); + + // Comment on course page. + $context = context_course::instance($this->course->id); + $args = new stdClass; + $args->context = $context; + $args->course = $this->course; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + $newcomment = $comment->add('New comment'); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $comment->delete($newcomment->id); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\block_comments\event\comment_deleted', $event); + $this->assertEquals($context, $event->get_context()); + $url = new moodle_url('/course/view.php', array('id' => $this->course->id)); + $this->assertEquals($url, $event->get_url()); + + // Comments when block is on module (wiki) page. + $context = context_module::instance($this->wiki->cmid); + $args = new stdClass; + $args->context = $context; + $args->course = $this->course; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + $newcomment = $comment->add('New comment 1'); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $comment->delete($newcomment->id); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\block_comments\event\comment_deleted', $event); + $this->assertEquals($context, $event->get_context()); + $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid)); + $this->assertEquals($url, $event->get_url()); + $this->assertEventContextNotUsed($event); + } +} diff --git a/comments/tests/privacy_provider_test.php b/comments/tests/privacy_provider_test.php new file mode 100644 index 0000000..9cc028b --- /dev/null +++ b/comments/tests/privacy_provider_test.php @@ -0,0 +1,468 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_comments + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use block_comments\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class block_comments_privacy_provider_testcase. + * + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_comments_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + /** @var stdClass A student who is only enrolled in course1. */ + protected $student1; + + /** @var stdClass A student who is only enrolled in course2. */ + protected $student2; + + /** @var stdClass A student who is enrolled in both course1 and course2. */ + protected $student12; + + /** @var stdClass A test course. */ + protected $course1; + + /** @var stdClass A test course. */ + protected $course2; + + protected function setUp() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create courses. + $generator = $this->getDataGenerator(); + $this->course1 = $generator->create_course(); + $this->course2 = $generator->create_course(); + + // Create and enrol students. + $this->student1 = $generator->create_user(); + $this->student2 = $generator->create_user(); + $this->student12 = $generator->create_user(); + + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $generator->enrol_user($this->student1->id, $this->course1->id, $studentrole->id); + $generator->enrol_user($this->student2->id, $this->course2->id, $studentrole->id); + $generator->enrol_user($this->student12->id, $this->course1->id, $studentrole->id); + $generator->enrol_user($this->student12->id, $this->course2->id, $studentrole->id); + + // Comment block on course pages. + $block = $this->add_comments_block_in_context(context_course::instance($this->course1->id)); + $block = $this->add_comments_block_in_context(context_course::instance($this->course2->id)); + } + + /** + * Posts a comment on a given context. + * + * @param string $text The comment's text. + * @param context $context The context on which we want to put the comment. + */ + protected function add_comment($text, context $context) { + $args = new stdClass; + $args->context = $context; + $args->area = 'page_comments'; + $args->itemid = 0; + $args->component = 'block_comments'; + $args->linktext = get_string('showcomments'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + + $comment->add($text); + } + + /** + * Creates a comments block on a context. + * + * @param context $context The context on which we want to put the block. + * @return block_base The created block instance. + * @throws coding_exception + */ + protected function add_comments_block_in_context(context $context) { + global $DB; + + $course = null; + + $page = new \moodle_page(); + $page->set_context($context); + + switch ($context->contextlevel) { + case CONTEXT_SYSTEM: + $page->set_pagelayout('frontpage'); + $page->set_pagetype('site-index'); + break; + case CONTEXT_COURSE: + $page->set_pagelayout('standard'); + $page->set_pagetype('course-view'); + $course = $DB->get_record('course', ['id' => $context->instanceid]); + $page->set_course($course); + break; + case CONTEXT_MODULE: + $page->set_pagelayout('standard'); + $mod = $DB->get_field_sql("SELECT m.name + FROM {modules} m + JOIN {course_modules} cm on cm.module = m.id + WHERE cm.id = ?", [$context->instanceid]); + $page->set_pagetype("mod-$mod-view"); + break; + case CONTEXT_USER: + $page->set_pagelayout('mydashboard'); + $page->set_pagetype('my-index'); + break; + default: + throw new coding_exception('Unsupported context for test'); + } + + $page->blocks->load_blocks(); + + $page->blocks->add_block_at_end_of_default_region('comments'); + + // We need to use another page object as load_blocks() only loads the blocks once. + $page2 = new \moodle_page(); + $page2->set_context($page->context); + $page2->set_pagelayout($page->pagelayout); + $page2->set_pagetype($page->pagetype); + if ($course) { + $page2->set_course($course); + } + + $page->blocks->load_blocks(); + $page2->blocks->load_blocks(); + $blocks = $page2->blocks->get_blocks_for_region($page2->blocks->get_default_region()); + $block = end($blocks); + + $block = block_instance('comments', $block->instance); + + return $block; + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('block_comments'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(1, $itemcollection); + + $link = reset($itemcollection); + + $this->assertEquals('core_comment', $link->get_name()); + $this->assertEmpty($link->get_privacy_fields()); + $this->assertEquals('privacy:metadata:core_comment', $link->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid() when user had not posted any comments.. + */ + public function test_get_contexts_for_userid_no_comment() { + $this->setUser($this->student1); + $coursecontext1 = context_course::instance($this->course1->id); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->student2); + $contextlist = provider::get_contexts_for_userid($this->student2->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->student12); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + $contextlist = provider::get_contexts_for_userid($this->student12->id); + $this->assertCount(2, $contextlist); + + $contextids = $contextlist->get_contextids(); + $this->assertEquals([$coursecontext1->id, $coursecontext2->id], $contextids, '', 0.0, 10, true); + } + + /** + * Test for provider::export_user_data() when the user has not posted any comments. + */ + public function test_export_for_context_no_comment() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->student1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->student2); + + $this->setUser($this->student2); + $this->export_context_data_for_user($this->student2->id, $coursecontext2, 'block_comments'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext2); + $this->assertFalse($writer->has_any_data()); + + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->student12); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Export all of the data for the context. + $this->export_context_data_for_user($this->student12->id, $coursecontext1, 'block_comments'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext1); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->student1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->student2); + $this->add_comment('New comment', $coursecontext2); + + $this->setUser($this->student12); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2. + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id]) + ); + + // Delete data based on context. + provider::delete_data_for_all_users_in_context($coursecontext1); + + // After deletion, the comments for $coursecontext1 should have been deleted. + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id]) + ); + } + + /** + * Test for provider::delete_data_for_all_users_in_context() when there are also comments from other plugins. + */ + public function test_delete_data_for_all_users_in_context_with_comments_from_other_plugins() { + global $DB; + + $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance = $assigngenerator->create_instance(['course' => $this->course1]); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $assigncontext = \context_module::instance($cm->id); + $assign = new \assign($assigncontext, $cm, $this->course1); + + // Add a comments block in the assignment page. + $this->add_comments_block_in_context($assigncontext); + + $submission = $assign->get_user_submission($this->student1->id, true); + + $options = new stdClass(); + $options->area = 'submission_comments'; + $options->course = $assign->get_course(); + $options->context = $assigncontext; + $options->itemid = $submission->id; + $options->component = 'assignsubmission_comments'; + $options->showcount = true; + $options->displaycancel = true; + + $comment = new comment($options); + $comment->set_post_permission(true); + + $this->setUser($this->student1); + $comment->add('Comment from student 1'); + + $this->add_comment('New comment', $assigncontext); + + $this->setUser($this->student2); + $this->add_comment('New comment', $assigncontext); + + // Before deletion, we should have 3 comments in $assigncontext. + // One comment is for the assignment submission and 2 are for the comments block. + $this->assertEquals( + 3, + $DB->count_records('comments', ['contextid' => $assigncontext->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $assigncontext->id]) + ); + + provider::delete_data_for_all_users_in_context($assigncontext); + + // After deletion, the comments for $assigncontext in the comment block should have been deleted, + // but the assignment submission comment should be left. + $this->assertEquals( + 1, + $DB->count_records('comments', ['contextid' => $assigncontext->id]) + ); + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $assigncontext->id]) + ); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->student1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->student2); + $this->add_comment('New comment', $coursecontext2); + + $this->setUser($this->student12); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2, + // and 3 comments by student12 in $coursecontext1 and $coursecontext2 combined. + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id]) + ); + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student12->id]) + ); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student12, 'block_comments', + [$coursecontext1->id, $coursecontext2->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the comments for the student12 should have been deleted. + $this->assertEquals( + 1, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 1, + $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id]) + ); + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student12->id]) + ); + } + + /** + * Test for provider::delete_data_for_user() when there are also comments from other plugins. + */ + public function test_delete_data_for_user_with_comments_from_other_plugins() { + global $DB; + + $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance = $assigngenerator->create_instance(['course' => $this->course1]); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $assigncontext = \context_module::instance($cm->id); + $assign = new \assign($assigncontext, $cm, $this->course1); + + // Add a comments block in the assignment page. + $this->add_comments_block_in_context($assigncontext); + + $submission = $assign->get_user_submission($this->student1->id, true); + + $options = new stdClass(); + $options->area = 'submission_comments'; + $options->course = $assign->get_course(); + $options->context = $assigncontext; + $options->itemid = $submission->id; + $options->component = 'assignsubmission_comments'; + $options->showcount = true; + $options->displaycancel = true; + + $comment = new comment($options); + $comment->set_post_permission(true); + + $this->setUser($this->student1); + $comment->add('Comment from student 1'); + + $this->add_comment('New comment', $assigncontext); + $this->add_comment('New comment', $assigncontext); + + // Before deletion, we should have 3 comments in $assigncontext. + // one comment is for the assignment submission and 2 are for the comments block. + $this->assertEquals( + 3, + $DB->count_records('comments', ['contextid' => $assigncontext->id]) + ); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'block_comments', + [$assigncontext->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the comments for the student1 in the comment block should have been deleted, + // but the assignment submission comment should be left. + $this->assertEquals( + 1, + $DB->count_records('comments', ['contextid' => $assigncontext->id]) + ); + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student1->id]) + ); + } +} diff --git a/comments/version.php b/comments/version.php new file mode 100644 index 0000000..9982f0e --- /dev/null +++ b/comments/version.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/>. + +/** + * Version details + * + * @package block_comments + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_comments'; // Full name of the plugin (used for diagnostics) diff --git a/community/block_community.php b/community/block_community.php new file mode 100644 index 0000000..fc345ca --- /dev/null +++ b/community/block_community.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/>. + +/** + * @package block_community + * @author Jerome Mouneyrac <jerome@mouneyrac.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * The community block + */ + +class block_community extends block_list { + + function init() { + $this->title = get_string('pluginname', 'block_community'); + } + + function user_can_addto($page) { + // Don't allow people to add the block if they can't even use it + if (!has_capability('moodle/community:add', $page->context)) { + return false; + } + + return parent::user_can_addto($page); + } + + function user_can_edit() { + // Don't allow people to edit the block if they can't even use it + if (!has_capability('moodle/community:add', + context::instance_by_id($this->instance->parentcontextid))) { + return false; + } + return parent::user_can_edit(); + } + + function get_content() { + global $CFG, $OUTPUT, $USER; + + $coursecontext = context::instance_by_id($this->instance->parentcontextid); + + if (!has_capability('moodle/community:add', $coursecontext) + or $this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass(); + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + if (!isloggedin()) { + return $this->content; + } + + $icon = $OUTPUT->pix_icon('i/group', get_string('group')); + $addcourseurl = new moodle_url('/blocks/community/communitycourse.php', + array('add' => true, 'courseid' => $this->page->course->id)); + $searchlink = html_writer::tag('a', $icon . get_string('addcourse', 'block_community'), + array('href' => $addcourseurl->out(false))); + $this->content->items[] = $searchlink; + + require_once($CFG->dirroot . '/blocks/community/locallib.php'); + $communitymanager = new block_community_manager(); + $courses = $communitymanager->block_community_get_courses($USER->id); + if ($courses) { + $this->content->items[] = html_writer::empty_tag('hr'); + $this->content->icons[] = ''; + $this->content->items[] = get_string('mycommunities', 'block_community'); + $this->content->icons[] = ''; + foreach ($courses as $course) { + //delete link + $deleteicon = $OUTPUT->pix_icon('t/delete', get_string('removecommunitycourse', 'block_community')); + $deleteurl = new moodle_url('/blocks/community/communitycourse.php', + array('remove' => true, + 'courseid' => $this->page->course->id, + 'communityid' => $course->id, 'sesskey' => sesskey())); + $deleteatag = html_writer::tag('a', $deleteicon, array('href' => $deleteurl)); + + $courselink = html_writer::tag('a', $course->coursename, + array('href' => $course->courseurl)); + $this->content->items[] = $courselink . ' ' . $deleteatag; + $this->content->icons[] = ''; + } + } + + return $this->content; + } + +} + diff --git a/community/classes/privacy/provider.php b/community/classes/privacy/provider.php new file mode 100644 index 0000000..5699066 --- /dev/null +++ b/community/classes/privacy/provider.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/>. + +/** + * Privacy Subsystem implementation for block_community. + * + * @package block_community + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_community\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; + +/** + * Privacy Subsystem implementation for block_community. + * + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { + + /** + * Returns information about how block_community stores its data. + * + * @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( + 'block_community', + [ + 'coursename' => 'privacy:metadata:block_community:coursename', + 'coursedescription' => 'privacy:metadata:block_community:coursedescription', + 'courseurl' => 'privacy:metadata:block_community:courseurl', + 'imageurl' => 'privacy:metadata:block_community:imageurl', + 'userid' => 'privacy:metadata:block_community:userid', + ], + 'privacy:metadata:block_community' + ); + + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new \core_privacy\local\request\contextlist(); + + // The block_community data is associated at the user context level, so retrieve the user's context id. + $sql = "SELECT c.id + FROM {block_community} bc + JOIN {context} c ON c.instanceid = bc.userid AND c.contextlevel = :contextuser + WHERE bc.userid = :userid + GROUP BY c.id"; + + $params = [ + 'contextuser' => CONTEXT_USER, + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export all user data for the specified user using the User context level. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + // If the user has block_community data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + // The block_community data export is organised in: {User Context}/Community Finder/My communities/data.json. + $subcontext = [ + get_string('pluginname', 'block_community'), + get_string('mycommunities', 'block_community') + ]; + + $sql = "SELECT bc.id as id, + bc.coursename as name, + bc.coursedescription as description, + bc.courseurl as url, + bc.imageurl as imageurl + FROM {block_community} bc + WHERE bc.userid = :userid + ORDER BY bc.coursename"; + + $params = [ + 'userid' => $userid + ]; + + $communities = $DB->get_records_sql($sql, $params); + + $data = (object) [ + 'communities' => $communities + ]; + + writer::with_context($context)->export_data($subcontext, $data); + } + + /** + * 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; + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $DB->delete_records('block_community', ['userid' => $userid]); + } + + /** + * Delete all user data for the specified user. + * + * @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; + + // If the user has block_community data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $DB->delete_records('block_community', ['userid' => $userid]); + } + +} diff --git a/community/communitycourse.php b/community/communitycourse.php new file mode 100644 index 0000000..67f024b --- /dev/null +++ b/community/communitycourse.php @@ -0,0 +1,208 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 various actions of the block. + * + * This page display the community course search form. + * It also handles adding a course to the community block. + * It also handles downloading a course template. + * + * @package block_community + * @author Jerome Mouneyrac <jerome@mouneyrac.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + */ + +require('../../config.php'); +require_once($CFG->dirroot . '/blocks/community/locallib.php'); +require_once($CFG->dirroot . '/blocks/community/forms.php'); + +require_login(); +$courseid = required_param('courseid', PARAM_INT); //if no courseid is given +$parentcourse = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + +$context = context_course::instance($courseid); +$PAGE->set_course($parentcourse); +$PAGE->set_url('/blocks/community/communitycourse.php'); +$PAGE->set_heading($SITE->fullname); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_title(get_string('searchcourse', 'block_community')); +$PAGE->navbar->add(get_string('searchcourse', 'block_community')); + +$search = optional_param('search', null, PARAM_TEXT); + +//if no capability to search course, display an error message +require_capability('moodle/community:add', $context); +$usercandownload = has_capability('moodle/community:download', $context); + +$communitymanager = new block_community_manager(); +$renderer = $PAGE->get_renderer('block_community'); + +/// Check if the page has been called with trust argument +$add = optional_param('add', -1, PARAM_INT); +$confirm = optional_param('confirmed', false, PARAM_INT); +if ($add != -1 and $confirm and confirm_sesskey()) { + $course = new stdClass(); + $course->name = optional_param('coursefullname', '', PARAM_TEXT); + $course->description = optional_param('coursedescription', '', PARAM_TEXT); + $course->url = optional_param('courseurl', '', PARAM_URL); + $course->imageurl = optional_param('courseimageurl', '', PARAM_URL); + $communitymanager->block_community_add_course($course, $USER->id); + echo $OUTPUT->header(); + echo $renderer->save_link_success( + new moodle_url('/course/view.php', array('id' => $courseid))); + echo $OUTPUT->footer(); + die(); +} + +/// Delete temp file when cancel restore +$cancelrestore = optional_param('cancelrestore', false, PARAM_INT); +if ($usercandownload and $cancelrestore and confirm_sesskey()) { + $filename = optional_param('filename', '', PARAM_ALPHANUMEXT); + //delete temp file + $backuptempdir = make_backup_temp_directory(''); + unlink($backuptempdir . '/' . $filename . ".mbz"); +} + +/// Download +$download = optional_param('download', -1, PARAM_INT); +$downloadcourseid = optional_param('downloadcourseid', '', PARAM_INT); +$coursefullname = optional_param('coursefullname', '', PARAM_ALPHANUMEXT); +$backupsize = optional_param('backupsize', 0, PARAM_INT); +if ($usercandownload and $download != -1 and !empty($downloadcourseid) and confirm_sesskey()) { + //OUTPUT: display restore choice page + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('downloadingcourse', 'block_community'), 3, 'main'); + $sizeinfo = new stdClass(); + $sizeinfo->total = number_format($backupsize / 1000000, 2); + echo html_writer::tag('div', get_string('downloadingsize', 'block_community', $sizeinfo), + array('class' => 'textinfo')); + if (ob_get_level()) { + ob_flush(); + } + flush(); + list($privatefilename, $tmpfilename) = \core\hub\publication::download_course_backup($downloadcourseid, $coursefullname); + echo html_writer::tag('div', get_string('downloaded', 'block_community'), + array('class' => 'textinfo')); + echo $OUTPUT->notification(get_string('downloadconfirmed', 'block_community', + $privatefilename), 'notifysuccess'); + echo $renderer->restore_confirmation_box($tmpfilename, $context); + echo $OUTPUT->footer(); + die(); +} + +/// Remove community +$remove = optional_param('remove', '', PARAM_INT); +$communityid = optional_param('communityid', '', PARAM_INT); +if ($remove != -1 and !empty($communityid) and confirm_sesskey()) { + $communitymanager->block_community_remove_course($communityid, $USER->id); + echo $OUTPUT->header(); + echo $renderer->remove_success(new moodle_url('/course/view.php', array('id' => $courseid))); + echo $OUTPUT->footer(); + die(); +} + +//Get form default/current values +$fromformdata['coverage'] = optional_param('coverage', 'all', PARAM_TEXT); +$fromformdata['licence'] = optional_param('licence', 'all', PARAM_ALPHANUMEXT); +$fromformdata['subject'] = optional_param('subject', 'all', PARAM_ALPHANUMEXT); +$fromformdata['audience'] = optional_param('audience', 'all', PARAM_ALPHANUMEXT); +$fromformdata['language'] = optional_param('language', current_language(), PARAM_ALPHANUMEXT); +$fromformdata['educationallevel'] = optional_param('educationallevel', 'all', PARAM_ALPHANUMEXT); +$fromformdata['downloadable'] = optional_param('downloadable', $usercandownload, PARAM_ALPHANUM); +$fromformdata['orderby'] = optional_param('orderby', 'newest', PARAM_ALPHA); +$fromformdata['search'] = $search; +$fromformdata['courseid'] = $courseid; +$hubselectorform = new community_hub_search_form('', $fromformdata); +$hubselectorform->set_data($fromformdata); + +//Retrieve courses by web service +$courses = null; +if (optional_param('executesearch', 0, PARAM_INT) and confirm_sesskey()) { + $downloadable = optional_param('downloadable', false, PARAM_INT); + + $options = new stdClass(); + if (!empty($fromformdata['coverage'])) { + $options->coverage = $fromformdata['coverage']; + } + if ($fromformdata['licence'] != 'all') { + $options->licenceshortname = $fromformdata['licence']; + } + if ($fromformdata['subject'] != 'all') { + $options->subject = $fromformdata['subject']; + } + if ($fromformdata['audience'] != 'all') { + $options->audience = $fromformdata['audience']; + } + if ($fromformdata['educationallevel'] != 'all') { + $options->educationallevel = $fromformdata['educationallevel']; + } + if ($fromformdata['language'] != 'all') { + $options->language = $fromformdata['language']; + } + + $options->orderby = $fromformdata['orderby']; + + //the range of course requested + $options->givememore = optional_param('givememore', 0, PARAM_INT); + + list($courses, $coursetotal) = \core\hub\publication::search($search, $downloadable, $options); +} + +// OUTPUT +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('searchcommunitycourse', 'block_community'), 3, 'main'); +echo $renderer->moodlenet_info(); + +$hubselectorform->display(); +if (!empty($errormessage)) { + echo $errormessage; +} + +//load javascript +$commentedcourseids = array(); //result courses with comments only +$courseids = array(); //all result courses +$courseimagenumbers = array(); //number of screenshots of all courses (must be exact same order than $courseids) +if (!empty($courses)) { + foreach ($courses as $course) { + if (!empty($course['comments'])) { + $commentedcourseids[] = $course['id']; + } + $courseids[] = $course['id']; + $courseimagenumbers[] = $course['screenshots']; + } +} +$PAGE->requires->yui_module('moodle-block_community-comments', 'M.blocks_community.init_comments', + array(array('commentids' => $commentedcourseids, 'closeButtonTitle' => get_string('close', 'editor')))); +$PAGE->requires->yui_module('moodle-block_community-imagegallery', 'M.blocks_community.init_imagegallery', + array(array('imageids' => $courseids, 'imagenumbers' => $courseimagenumbers, + 'huburl' => HUB_MOODLEORGHUBURL, 'closeButtonTitle' => get_string('close', 'editor')))); + +echo highlight($search, $renderer->course_list($courses, null, $courseid)); + +//display givememore/Next link if more course can be displayed +if (!empty($courses)) { + if (($options->givememore + count($courses)) < $coursetotal) { + $fromformdata['givememore'] = count($courses) + $options->givememore; + $fromformdata['executesearch'] = true; + $fromformdata['sesskey'] = sesskey(); + echo $renderer->next_button($fromformdata); + } +} + +echo $OUTPUT->footer(); diff --git a/community/db/access.php b/community/db/access.php new file mode 100644 index 0000000..1ec36ed --- /dev/null +++ b/community/db/access.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/>. + +/** + * Community block caps. + * + * @package block_community + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/community:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/community: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/community/db/install.xml b/community/db/install.xml new file mode 100644 index 0000000..58705d7 --- /dev/null +++ b/community/db/install.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="blocks/community/db" VERSION="20120122" COMMENT="XMLDB file for Moodle blocks/community" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="block_community" COMMENT="Community block"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="coursename" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="coursedescription" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="courseurl" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="imageurl" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/community/db/upgrade.php b/community/db/upgrade.php new file mode 100644 index 0000000..d01face --- /dev/null +++ b/community/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the community block + * + * 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. + * + * @since Moodle 2.0 + * @package block_community + * @copyright 2010 Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * + * @param int $oldversion + */ +function xmldb_block_community_upgrade($oldversion) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/community/forms.php b/community/forms.php new file mode 100644 index 0000000..8142b5c --- /dev/null +++ b/community/forms.php @@ -0,0 +1,173 @@ +<?php +/////////////////////////////////////////////////////////////////////////// +// // +// This file is part of Moodle - http://moodle.org/ // +// Moodle - Modular Object-Oriented Dynamic Learning Environment // +// // +// Moodle is free software: you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation, either version 3 of the License, or // +// (at your option) any later version. // +// // +// Moodle is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License for more 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 community search + * + * @package block_community + * @author Jerome Mouneyrac <jerome@mouneyrac.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + */ + +require_once($CFG->libdir . '/formslib.php'); + +class community_hub_search_form extends moodleform { + + public function definition() { + global $CFG; + $mform = & $this->_form; + + //set default value + $search = $this->_customdata['search']; + if (isset($this->_customdata['coverage'])) { + $coverage = $this->_customdata['coverage']; + } else { + $coverage = 'all'; + } + if (isset($this->_customdata['licence'])) { + $licence = $this->_customdata['licence']; + } else { + $licence = 'all'; + } + if (isset($this->_customdata['subject'])) { + $subject = $this->_customdata['subject']; + } else { + $subject = 'all'; + } + if (isset($this->_customdata['audience'])) { + $audience = $this->_customdata['audience']; + } else { + $audience = 'all'; + } + if (isset($this->_customdata['language'])) { + $language = $this->_customdata['language']; + } else { + $language = current_language(); + } + if (isset($this->_customdata['educationallevel'])) { + $educationallevel = $this->_customdata['educationallevel']; + } else { + $educationallevel = 'all'; + } + if (isset($this->_customdata['downloadable'])) { + $downloadable = $this->_customdata['downloadable']; + } else { + $downloadable = 1; + } + if (isset($this->_customdata['orderby'])) { + $orderby = $this->_customdata['orderby']; + } else { + $orderby = 'newest'; + } + + $mform->addElement('header', 'site', get_string('search', 'block_community')); + + //add the course id (of the context) + $mform->addElement('hidden', 'courseid', $this->_customdata['courseid']); + $mform->setType('courseid', PARAM_INT); + $mform->addElement('hidden', 'executesearch', 1); + $mform->setType('executesearch', PARAM_INT); + + // Display enrol/download select box if the USER has the download capability on the course. + if (has_capability('moodle/community:download', + context_course::instance($this->_customdata['courseid']))) { + $options = array(0 => get_string('enrollable', 'block_community'), + 1 => get_string('downloadable', 'block_community')); + $mform->addElement('select', 'downloadable', get_string('enroldownload', 'block_community'), + $options); + $mform->addHelpButton('downloadable', 'enroldownload', 'block_community'); + + $mform->setDefault('downloadable', $downloadable); + } else { + $mform->addElement('hidden', 'downloadable', 0); + } + $mform->setType('downloadable', PARAM_INT); + + $options = \core\hub\publication::audience_options(true); + $mform->addElement('select', 'audience', get_string('audience', 'block_community'), $options); + $mform->setDefault('audience', $audience); + unset($options); + $mform->addHelpButton('audience', 'audience', 'block_community'); + + $options = \core\hub\publication::educational_level_options(true); + $mform->addElement('select', 'educationallevel', + get_string('educationallevel', 'block_community'), $options); + $mform->setDefault('educationallevel', $educationallevel); + unset($options); + $mform->addHelpButton('educationallevel', 'educationallevel', 'block_community'); + + $options = \core\hub\publication::get_sorted_subjects(); + $mform->addElement('searchableselector', 'subject', get_string('subject', 'block_community'), + $options, array('id' => 'communitysubject')); + $mform->setDefault('subject', $subject); + unset($options); + $mform->addHelpButton('subject', 'subject', 'block_community'); + + require_once($CFG->libdir . "/licenselib.php"); + $licensemanager = new license_manager(); + $licences = $licensemanager->get_licenses(); + $options = array(); + $options['all'] = get_string('any'); + foreach ($licences as $license) { + $options[$license->shortname] = get_string($license->shortname, 'license'); + } + $mform->addElement('select', 'licence', get_string('licence', 'block_community'), $options); + unset($options); + $mform->addHelpButton('licence', 'licence', 'block_community'); + $mform->setDefault('licence', $licence); + + $languages = get_string_manager()->get_list_of_languages(); + core_collator::asort($languages); + $languages = array_merge(array('all' => get_string('any')), $languages); + $mform->addElement('select', 'language', get_string('language'), $languages); + + $mform->setDefault('language', $language); + $mform->addHelpButton('language', 'language', 'block_community'); + + $mform->addElement('select', 'orderby', get_string('orderby', 'block_community'), + array('newest' => get_string('orderbynewest', 'block_community'), + 'eldest' => get_string('orderbyeldest', 'block_community'), + 'fullname' => get_string('orderbyname', 'block_community'), + 'publisher' => get_string('orderbypublisher', 'block_community'), + 'ratingaverage' => get_string('orderbyratingaverage', 'block_community'))); + + $mform->setDefault('orderby', $orderby); + $mform->addHelpButton('orderby', 'orderby', 'block_community'); + $mform->setType('orderby', PARAM_ALPHA); + + $mform->setAdvanced('audience'); + $mform->setAdvanced('educationallevel'); + $mform->setAdvanced('subject'); + $mform->setAdvanced('licence'); + $mform->setAdvanced('language'); + $mform->setAdvanced('orderby'); + + $mform->addElement('text', 'search', get_string('keywords', 'block_community'), + array('size' => 30)); + $mform->addHelpButton('search', 'keywords', 'block_community'); + $mform->setType('search', PARAM_NOTAGS); + + $mform->addElement('submit', 'submitbutton', get_string('search', 'block_community')); + + } + +} diff --git a/community/lang/en/block_community.php b/community/lang/en/block_community.php new file mode 100644 index 0000000..73e65d4 --- /dev/null +++ b/community/lang/en/block_community.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/>. + +/** + * Strings for component 'block_community', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_community + * @author Jerome Mouneyrac <jerome@mouneyrac.com> + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['activities'] = 'Activities'; +$string['add'] = 'Add'; +$string['addedtoblock'] = 'A link to this course has been added in your community finder block'; +$string['addtocommunityblock'] = 'Save a link to this course'; +$string['addcommunitycourse'] = 'Add community course'; +$string['additionalcoursedesc'] = '{$a->lang} Creator: {$a->creatorname} - Publisher: {$a->publishername} - Subject: {$a->subject} + - Audience: {$a->audience} - Educational level: {$a->educationallevel} - License: {$a->license}'; +$string['addcourse'] = 'Search'; +$string['audience'] = 'Designed for'; +$string['audience_help'] = 'What kind of course are you looking for? As well as traditional courses intended for students, you might search for communities of Educators or Moodle Administrators'; +$string['blocks'] = 'Blocks'; +$string['cannotselecttopsubject'] = 'Cannot select a top subject level'; +$string['comments'] = 'Comments ({$a})'; +$string['community:addinstance'] = 'Add a new community finder block'; +$string['community:myaddinstance'] = 'Add a new community finder block to Dashboard'; +$string['contentinfo'] = 'Subject: {$a->subject} - Audience: {$a->audience} - Educational level: {$a->educationallevel}'; +$string['continue'] = 'Continue'; +$string['contributors'] = ' - Contributors: {$a}'; +$string['coursedesc'] = 'Description'; +$string['courselang'] = 'Language'; +$string['coursename'] = 'Name'; +$string['courses'] = 'Courses'; +$string['coverage'] = 'Tags: {$a}'; +$string['donotrestore'] = 'No'; +$string['dorestore'] = 'Yes'; +$string['download'] = 'Download'; +$string['downloadable'] = 'courses I can download'; +$string['downloadablecourses'] = 'Downloadable courses'; +$string['downloadconfirmed'] = 'The backup has been saved in your private files {$a}'; +$string['downloaded'] = '...finished.'; +$string['downloadingcourse'] = 'Downloading course'; +$string['downloadingsize'] = 'Please wait the course file is downloading ({$a->total}Mb)...'; +$string['downloadtemplate'] = 'Create course from template'; +$string['educationallevel'] = 'Educational level'; +$string['educationallevel_help'] = 'What educational level are you searching for? In the case of communities of educators, this level describes the level they are teaching.'; +$string['enroldownload'] = 'Find'; +$string['enroldownload_help'] = 'Some courses listed in the selected hub are being advertised so that people can come and participate in them on the original site. + +Others are course templates provided for you to download and use on your own Moodle site.'; +$string['enrollable'] = 'courses I can enrol in'; +$string['enrollablecourses'] = 'Enrollable courses'; +$string['errorcourselisting'] = 'An error occurred when retrieving the course listing from the selected hub, please try again later. ({$a})'; +$string['errorhublisting'] = 'An error occurred when retrieving the hub listing from Moodle.org, please try again later. ({$a})'; +$string['fileinfo'] = 'Language: {$a->lang} - License: {$a->license} - Time updated: {$a->timeupdated}'; +$string['hideall'] = 'Hide hubs'; +$string['hub'] = 'hub'; +$string['hubnottrusted'] = 'Not trusted'; +$string['hubtrusted'] = 'This hub is trusted by Moodle.org'; +$string['install'] = 'Install'; +$string['keywords'] = 'Keywords'; +$string['keywords_help'] = 'You can search for courses containing specific text in the name, description and other fields of the database.'; +$string['langdesc'] = 'Language: {$a} - '; +$string['language'] = 'Language'; +$string['language_help'] = 'You can search for courses written in a specific language.'; +$string['licence'] = 'License'; +$string['licence_help'] = 'You can search for courses that are licensed in a particular way.'; +$string['moredetails'] = 'More details'; +$string['mycommunities'] = 'My communities:'; +$string['next'] = 'Next >>>'; +$string['nocomments'] = 'No comments'; +$string['nocourse'] = 'No courses found'; +$string['noratings'] = 'No ratings'; +$string['operation'] = 'Operation'; +$string['orderby'] = 'Sort by'; +$string['orderby_help'] = 'The order the search results are displayed.'; +$string['orderbynewest'] = 'Newest'; +$string['orderbyeldest'] = 'Oldest'; +$string['orderbyname'] = 'Name'; +$string['orderbypublisher'] = 'Publisher'; +$string['orderbyratingaverage'] = 'Rating'; +$string['outcomes'] = 'Outcomes: {$a}'; +$string['pluginname'] = 'Community finder'; +$string['privacy:metadata:block_community'] = 'The Community block stores links to shared community courses users can enrol in.'; +$string['privacy:metadata:block_community:coursename'] = 'The name of the linked community course.'; +$string['privacy:metadata:block_community:coursedescription'] = 'The description of the linked community course.'; +$string['privacy:metadata:block_community:courseurl'] = 'The course URL of the linked community course.'; +$string['privacy:metadata:block_community:imageurl'] = 'The image URL of the linked community course.'; +$string['privacy:metadata:block_community:userid'] = 'The ID of the user who created the linked community course.'; +$string['rateandcomment'] = 'Rate and comment'; +$string['rating'] = 'Rating'; +$string['removecommunitycourse'] = 'Remove community course'; +$string['restorecourse'] = 'Restore course'; +$string['restorecourseinfo'] = 'Restore the course?'; +$string['screenshots'] = 'Screenshots'; +$string['search'] = 'Search'; +$string['searchcommunitycourse'] = 'Search for community course'; +$string['searchcourse'] = 'Search for community course'; +$string['selecthub'] = 'Select hub'; +$string['selecthub_help'] = 'Select hub where to search the courses.'; +$string['sites'] = 'Sites'; +$string['showall'] = 'Show all hubs'; +$string['subject'] = 'Subject'; +$string['subject_help'] = 'To narrow your search to courses about a particular subject, choose one from this list.'; +$string['userinfo'] = 'Creator: {$a->creatorname} - Publisher: {$a->publishername}'; +$string['visitdemo'] = 'Visit demo'; +$string['visitsite'] = 'Visit site'; diff --git a/community/locallib.php b/community/locallib.php new file mode 100644 index 0000000..d975078 --- /dev/null +++ b/community/locallib.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/>. + +/** + * Community library + * + * @package block_community + * @author Jerome Mouneyrac <jerome@mouneyrac.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * + */ + +class block_community_manager { + + /** + * Add a community course + * @param object $course + * @param integer $userid + * @return id of course or false if already added + */ + public function block_community_add_course($course, $userid) { + global $DB; + + $community = $this->block_community_get_course($course->url, $userid); + + if (empty($community)) { + $community = new stdClass(); + $community->userid = $userid; + $community->coursename = $course->name; + $community->coursedescription = $course->description; + $community->courseurl = $course->url; + $community->imageurl = $course->imageurl; + return $DB->insert_record('block_community', $community); + } else { + return false; + } + } + + /** + * Return all community courses of a user + * @param integer $userid + * @return array of course + */ + public function block_community_get_courses($userid) { + global $DB; + return $DB->get_records('block_community', array('userid' => $userid), 'coursename'); + } + + /** + * Return a community courses of a user + * @param integer $userid + * @param integer $userid + * @return array of course + */ + public function block_community_get_course($courseurl, $userid) { + global $DB; + return $DB->get_record('block_community', + array('courseurl' => $courseurl, 'userid' => $userid)); + } + + /** + * Delete a community course + * @param integer $communityid + * @param integer $userid + * @return bool true + */ + public function block_community_remove_course($communityid, $userid) { + global $DB, $USER; + return $DB->delete_records('block_community', + array('userid' => $userid, 'id' => $communityid)); + } + +} diff --git a/community/renderer.php b/community/renderer.php new file mode 100644 index 0000000..f415204 --- /dev/null +++ b/community/renderer.php @@ -0,0 +1,400 @@ +<?php + +/////////////////////////////////////////////////////////////////////////// +// // +// This file is part of Moodle - http://moodle.org/ // +// Moodle - Modular Object-Oriented Dynamic Learning Environment // +// // +// Moodle is free software: you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation, either version 3 of the License, or // +// (at your option) any later version. // +// // +// Moodle is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License for more 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 community renderer. + * @package block_community + * @copyright 2010 Moodle Pty Ltd (http://moodle.com) + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_community_renderer extends plugin_renderer_base { + + public function restore_confirmation_box($filename, $context) { + $restoreurl = new moodle_url('/backup/restore.php', + array('filename' => $filename . ".mbz", 'contextid' => $context->id)); + $searchurl = new moodle_url('/blocks/community/communitycourse.php', + array('add' => 1, 'courseid' => $context->instanceid, + 'cancelrestore' => 1, 'sesskey' => sesskey(), + 'filename' => $filename)); + $formrestore = new single_button($restoreurl, + get_string('dorestore', 'block_community')); + $formsearch = new single_button($searchurl, + get_string('donotrestore', 'block_community')); + return $this->output->confirm(get_string('restorecourseinfo', 'block_community'), + $formrestore, $formsearch); + } + + /** + * Display remove community success message and a button to be redirected to te referer page + * @param moodle_url $url the page to be redirected to + * @return string html + */ + public function remove_success(moodle_url $url) { + $html = $this->output->notification(get_string('communityremoved', 'hub'), + 'notifysuccess'); + $continuebutton = new single_button($url, + get_string('continue', 'block_community')); + $html .= html_writer::tag('div', $this->output->render($continuebutton), + array('class' => 'continuebutton')); + return $html; + } + + /** + * Display add community course success message and a button to be redirected to te referer page + * @param moodle_url $url the page to be redirected to + * @return string html + */ + public function save_link_success(moodle_url $url) { + $html = $this->output->notification(get_string('addedtoblock', 'block_community'), + 'notifysuccess'); + $continuebutton = new single_button($url, + get_string('continue', 'block_community')); + $html .= html_writer::tag('div', $this->output->render($continuebutton), + array('class' => 'continuebutton')); + return $html; + } + + /** + * The 'Next'/'more course result' link for a courses search + * @param array $data - the form parameter to execute the search on more result + * @return string html code + */ + public function next_button($data) { + $nextlink = html_writer::tag('a', get_string('next', 'block_community'), + array('href' => new moodle_url('', $data))); + return html_writer::tag('div', $nextlink, array( 'class' => 'nextlink')); + } + + /** + * Displays information about moodle.net above course search form + * + * @return string + */ + public function moodlenet_info() { + if (!$info = \core\hub\registration::get_moodlenet_info()) { + return ''; + } + + $image = html_writer::div(html_writer::img($info['imgurl'], $info['name']), 'hubimage'); + + $namelink = html_writer::link($info['url'], html_writer::tag('h2', $info['name']), array('class' => 'hubtitlelink')); + $description = clean_param($info['description'], PARAM_TEXT); + $descriptiontext = html_writer::div(format_text($description, FORMAT_PLAIN), 'hubdescription'); + + $additionaldesc = get_string('enrollablecourses', 'block_community') . ': ' . $info['enrollablecourses'] . ' - ' . + get_string('downloadablecourses', 'block_community') . ': ' . $info['downloadablecourses']; + $stats = html_writer::div(html_writer::tag('div', $additionaldesc), 'hubstats'); + + $text = html_writer::div($descriptiontext . $stats, 'hubtext'); + + $imgandtext = html_writer::div($image . $text, 'hubimgandtext'); + + $fulldesc = html_writer::div($namelink . $imgandtext, 'hubmainhmtl clearfix'); + + return html_writer::div($fulldesc, 'formlisting'); + } + + /** + * Display a list of courses + * @param array $courses + * @param mixed $unused parameter is not used + * @param int $contextcourseid context course id + * @return string + */ + public function course_list($courses, $unused, $contextcourseid) { + global $CFG; + + $renderedhtml = ''; + + if (empty($courses)) { + if (isset($courses)) { + $renderedhtml .= get_string('nocourse', 'block_community'); + } + } else { + $courseiteration = 0; + foreach ($courses as $course) { + $course = (object) $course; + $courseiteration = $courseiteration + 1; + + //create visit link html + if (!empty($course->courseurl)) { + $courseurl = new moodle_url($course->courseurl); + $linktext = get_string('visitsite', 'block_community'); + } else { + $courseurl = new moodle_url($course->demourl); + $linktext = get_string('visitdemo', 'block_community'); + } + + $visitlinkhtml = html_writer::tag('a', $linktext, + array('href' => $courseurl, 'class' => 'hubcoursedownload', + 'onclick' => 'this.target="_blank"')); + + //create title html + $coursename = html_writer::tag('h3', $course->fullname, + array('class' => 'hubcoursetitle')); + $coursenamehtml = html_writer::tag('div', $coursename, + array('class' => 'hubcoursetitlepanel')); + + // create screenshots html + $screenshothtml = ''; + if (!empty($course->screenshotbaseurl)) { + $screenshothtml = html_writer::empty_tag('img', + array('src' => $course->screenshotbaseurl, 'alt' => $course->fullname)); + } + $coursescreenshot = html_writer::tag('div', $screenshothtml, + array('class' => 'coursescreenshot', + 'id' => 'image-' . $course->id)); + + //create description html + $deschtml = html_writer::tag('div', $course->description, + array('class' => 'hubcoursedescription')); + + //create users related information html + $courseuserinfo = get_string('userinfo', 'block_community', $course); + if ($course->contributornames) { + $courseuserinfo .= ' - ' . get_string('contributors', 'block_community', + $course->contributornames); + } + $courseuserinfohtml = html_writer::tag('div', $courseuserinfo, + array('class' => 'hubcourseuserinfo')); + + //create course content related information html + $course->subject = (get_string_manager()->string_exists($course->subject, 'edufields')) ? + get_string($course->subject, 'edufields') : get_string('none'); + $course->audience = get_string('audience' . $course->audience, 'hub'); + $course->educationallevel = get_string('edulevel' . $course->educationallevel, 'hub'); + $coursecontentinfo = ''; + if (empty($course->coverage)) { + $course->coverage = ''; + } else { + $coursecontentinfo .= get_string('coverage', 'block_community', $course->coverage); + $coursecontentinfo .= ' - '; + } + $coursecontentinfo .= get_string('contentinfo', 'block_community', $course); + $coursecontentinfohtml = html_writer::tag('div', $coursecontentinfo, + array('class' => 'hubcoursecontentinfo')); + + ///create course file related information html + //language + if (!empty($course->language)) { + $languages = get_string_manager()->get_list_of_languages(); + $course->lang = $languages[$course->language]; + } else { + $course->lang = ''; + } + //licence + require_once($CFG->libdir . "/licenselib.php"); + $licensemanager = new license_manager(); + $licenses = $licensemanager->get_licenses(); + foreach ($licenses as $license) { + if ($license->shortname == $course->licenceshortname) { + $course->license = $license->fullname; + } + } + $course->timeupdated = userdate($course->timemodified); + $coursefileinfo = get_string('fileinfo', 'block_community', $course); + $coursefileinfohtml = html_writer::tag('div', $coursefileinfo, + array('class' => 'hubcoursefileinfo')); + + + + //Create course content html + $blocks = core_component::get_plugin_list('block'); + $activities = core_component::get_plugin_list('mod'); + if (!empty($course->contents)) { + $activitieshtml = ''; + $blockhtml = ''; + foreach ($course->contents as $content) { + $content = (object) $content; + if ($content->moduletype == 'block') { + if (!empty($blockhtml)) { + $blockhtml .= ' - '; + } + if (array_key_exists($content->modulename, $blocks)) { + $blockname = get_string('pluginname', 'block_' . $content->modulename); + } else { + $blockname = $content->modulename; + } + $blockhtml .= $blockname . " (" . $content->contentcount . ")"; + } else { + if (!empty($activitieshtml)) { + $activitieshtml .= ' - '; + } + if (array_key_exists($content->modulename, $activities)) { + $activityname = get_string('modulename', $content->modulename); + } else { + $activityname = $content->modulename; + } + $activitieshtml .= $activityname . " (" . $content->contentcount . ")"; + } + } + + $blocksandactivities = html_writer::tag('div', + get_string('activities', 'block_community') . " : " . $activitieshtml); + + //Uncomment following lines to display blocks information +// $blocksandactivities .= html_writer::tag('span', +// get_string('blocks', 'block_community') . " : " . $blockhtml); + } + + //Create outcomes html + $outcomes= ''; + if (!empty($course->outcomes)) { + foreach ($course->outcomes as $outcome) { + if (!empty($outcomes)) { + $outcomes .= ', '; + } + $outcomes .= $outcome['fullname']; + } + $outcomes = get_string('outcomes', 'block_community', + $outcomes); + } + $outcomeshtml = html_writer::tag('div', $outcomes, array('class' => 'hubcourseoutcomes')); + + //create additional information html + $additionaldesc = $courseuserinfohtml . $coursecontentinfohtml + . $coursefileinfohtml . $blocksandactivities . $outcomeshtml; + $additionaldeschtml = html_writer::tag('div', $additionaldesc, + array('class' => 'additionaldesc')); + + //Create add button html + $addbuttonhtml = ""; + if ($course->enrollable) { + $params = array('sesskey' => sesskey(), 'add' => 1, 'confirmed' => 1, + 'coursefullname' => $course->fullname, 'courseurl' => $courseurl, + 'coursedescription' => $course->description, + 'courseid' => $contextcourseid); + $addurl = new moodle_url("/blocks/community/communitycourse.php", $params); + $addbuttonhtml = html_writer::tag('a', + get_string('addtocommunityblock', 'block_community'), + array('href' => $addurl, 'class' => 'centeredbutton, hubcoursedownload')); + } + + //create download button html + $downloadbuttonhtml = ""; + if (!$course->enrollable) { + $params = array('sesskey' => sesskey(), 'download' => 1, 'confirmed' => 1, + 'remotemoodleurl' => $CFG->wwwroot, 'courseid' => $contextcourseid, + 'downloadcourseid' => $course->id, + 'coursefullname' => $course->fullname, 'backupsize' => $course->backupsize); + $downloadurl = new moodle_url("/blocks/community/communitycourse.php", $params); + $downloadbuttonhtml = html_writer::tag('a', get_string('install', 'block_community'), + array('href' => $downloadurl, 'class' => 'centeredbutton, hubcoursedownload')); + } + + //Create rating html + $rating = html_writer::tag('div', get_string('noratings', 'block_community'), + array('class' => 'norating')); + if (!empty($course->rating)) { + $course->rating = (object) $course->rating; + if ($course->rating->count > 0) { + + //calculate size of the rating star + $starimagesize = 20; //in px + $numberofstars = 5; + $size = ($course->rating->aggregate / $course->rating->scaleid) + * $numberofstars * $starimagesize; + $rating = html_writer::tag('li', '', + array('class' => 'current-rating', + 'style' => 'width:' . $size . 'px;')); + + $rating = html_writer::tag('ul', $rating, + array('class' => 'star-rating clearfix')); + $rating .= html_writer::tag('div', ' (' . $course->rating->count . ')', + array('class' => 'ratingcount clearfix')); + } + } + + + //Create comments html + $coursecomments = html_writer::tag('div', get_string('nocomments', 'block_community'), + array('class' => 'nocomments')); + $commentcount = 0; + if (!empty($course->comments)) { + //display only if there is some comment if there is some comment + $commentcount = count($course->comments); + $coursecomments = html_writer::tag('div', + get_string('comments', 'block_community', $commentcount), + array('class' => 'commenttitle')); + + foreach ($course->comments as $comment) { + $commentator = html_writer::tag('div', + $comment['commentator'], + array('class' => 'hubcommentator')); + $commentdate = html_writer::tag('div', + ' - ' . userdate($comment['date'], '%e/%m/%y'), + array('class' => 'hubcommentdate clearfix')); + + $commenttext = html_writer::tag('div', + $comment['comment'], + array('class' => 'hubcommenttext')); + + $coursecomments .= html_writer::tag('div', + $commentator . $commentdate . $commenttext, + array('class' => 'hubcomment')); + } + $coursecommenticon = html_writer::tag('div', + get_string('comments', 'block_community', $commentcount), + array('class' => 'hubcoursecomments', + 'id' => 'comments-' . $course->id)); + $coursecomments = $coursecommenticon . html_writer::tag('div', + $coursecomments, + array('class' => 'yui3-overlay-loading', + 'id' => 'commentoverlay-' . $course->id)); + } + + //link rate and comment + $rateandcomment = html_writer::tag('div', + html_writer::link($course->commenturl, get_string('rateandcomment', 'block_community'), + ['onclick' => 'this.target="_blank"']), + array('class' => 'hubrateandcomment')); + + //the main DIV tags + $buttonsdiv = html_writer::tag('div', + $addbuttonhtml . $downloadbuttonhtml . $visitlinkhtml, + array('class' => 'courseoperations')); + $screenshotbuttonsdiv = html_writer::tag('div', + $coursescreenshot . $buttonsdiv, + array('class' => 'courselinks')); + + $coursedescdiv = html_writer::tag('div', + $deschtml . $additionaldeschtml + . $rating . $coursecomments . $rateandcomment, + array('class' => 'coursedescription')); + $coursehtml = + $coursenamehtml . html_writer::tag('div', + $coursedescdiv . $screenshotbuttonsdiv, + array('class' => 'hubcourseinfo clearfix')); + + $renderedhtml .=html_writer::tag('div', $coursehtml, + array('class' => 'fullhubcourse clearfix')); + } + + $renderedhtml = html_writer::tag('div', $renderedhtml, + array('class' => 'hubcourseresult')); + } + + return $renderedhtml; + } + +} diff --git a/community/styles.css b/community/styles.css new file mode 100644 index 0000000..f9ecac8 --- /dev/null +++ b/community/styles.css @@ -0,0 +1,355 @@ +/** General display rules **/ + +/* HUB SELECTOR */ +#page-blocks-community-communitycourse .hubscreenshot { + float: left; +} + +#page-blocks-community-communitycourse .hubtitlelink { + color: #999; +} + +#page-blocks-community-communitycourse .hubsmalllogo { + padding-left: 3px; + padding-right: 7px; + float: left; +} + +#page-blocks-community-communitycourse .hubtext { + display: block; + width: 68%; + padding-left: 165px; +} + +#page-blocks-community-communitycourse .hubimage { + float: left; + display: block; + width: 100px; +} + +#page-blocks-community-communitycourse .hubstats { + padding-top: 10px; +} + +#page-blocks-community-communitycourse .hubstats .iconhelp { + float: left; + padding-right: 3px; +} + +#page-blocks-community-communitycourse .hubadditionaldesc { + color: #666; + font-size: 90%; + display: block; +} + +#page-blocks-community-communitycourse .hubscreenshot { + margin-right: 10px; +} + +#page-blocks-community-communitycourse .hubtrusted { + display: inline; +} + +#page-blocks-community-communitycourse .trustedtr { + background-color: #ffe1c3; +} + +#page-blocks-community-communitycourse .prioritisetr { + background-color: #ffd4ff; +} + +#page-blocks-community-communitycourse .blockdescription { + font-size: 80%; + color: #555; +} + +#page-blocks-community-communitycourse .trusted { + font-size: 90%; + color: #063; + font-weight: normal; + font-style: italic; +} + +/* COURSES RESULT */ +#page-blocks-community-communitycourse .additionaldesc { + font-size: 80%; + color: #8b8989; +} + +#page-blocks-community-communitycourse .comment-link { + font-size: 80%; + color: #555; +} + +#page-blocks-community-communitycourse .coursescreenshot { + text-align: center; + cursor: pointer; +} + +#page-blocks-community-communitycourse .hubcourseinfo { + margin-left: 15px; +} + +#page-blocks-community-communitycourse .pagingbar { + text-align: center; +} + +#page-blocks-community-communitycourse .coursecomment { + float: right; +} + +#page-blocks-community-communitycourse .courseoperations { + margin-top: 9px; + text-align: center; +} + +#page-blocks-community-communitycourse .hubcoursedownload:hover { + background-color: #cdc9c9; +} + +#page-blocks-community-communitycourse .courselinks { + float: right; + width: 180px; +} + +#page-blocks-community-communitycourse .ratingaggregate { + float: left; + padding-right: 4px; +} + +#page-blocks-community-communitycourse .hubcourserating { + padding-top: 3px; + font-size: 80%; + color: #555; +} + +#page-blocks-community-communitycourse .coursedescription { + width: 70%; + float: left; +} + +#page-blocks-community-communitycourse .fullhubcourse { + margin-bottom: 20px; +} + +#page-blocks-community-communitycourse .hubcoursetitlepanel { + margin-bottom: 6px; +} + +#page-blocks-community-communitycourse .hubcourseresult { + background: none repeat scroll 0 0 #fff; + clear: both; + margin: 30px auto 0; + z-index: 90; + width: 95%; + padding: 10px 10px 10px 10px; + border-style: solid; + border-width: 1px; +} + +#page-blocks-community-communitycourse .hubcoursetitle { + -webkit-box-shadow: rgba(0, 0, 0, 0.546875) 0 0 4px; + -moz-box-shadow: rgba(0, 0, 0, 0.546875) 0 0 4px; + background: #8b8989; + left: -15px; + position: relative; + z-index: 0; + border: 0; + margin: 0; + outline: 0; + padding: 0; + vertical-align: baseline; + color: #fff; + padding-top: 6px; + padding-bottom: 6px; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); + text-align: left; + font-style: italic; + font-weight: normal; + line-height: 1.2em; + font-size: 140%; + width: 102%; + text-indent: 15px; +} + +#page-blocks-community-communitycourse .hubcoursedownload { + display: inline-block; + padding: 5px 8px 6px; + color: black; + text-decoration: none; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + position: relative; + cursor: pointer; + background-color: #eee9e9; + margin-left: 6px; + font-size: 95%; + margin-bottom: 9px; +} + +/* STAR RATING */ +#page-blocks-community-communitycourse .ratingcount { + color: #8b8989; + font-size: 80%; + vertical-align: top; +} + +#page-blocks-community-communitycourse .norating { + font-weight: bold; + color: #8b8989; + font-size: 80%; +} + +#page-blocks-community-communitycourse .star-rating { + list-style: none; + margin: 4px 0 4px; + padding: 0; + width: 100px; + height: 20px; + position: relative; + background: url([[pix:i/star-rating]]) top left repeat-x; + float: left; +} + +#page-blocks-community-communitycourse .star-rating li { + padding: 0; + margin: 0; + height: 20px; + width: 20px; + float: left; +} + +#page-blocks-community-communitycourse .star-rating li.current-rating { + background: url([[pix:i/star-rating]]) left bottom; + position: absolute; + height: 20px; + display: block; + text-indent: -9000px; + z-index: 1; +} + +/* COMMENTS */ +#page-blocks-community-communitycourse .nocomments { + font-weight: bold; + color: #8b8989; + font-size: 80%; +} + +#page-blocks-community-communitycourse .hubcommentator { + float: left; + font-weight: bold; +} + +#page-blocks-community-communitycourse .hubcommentdate { + font-weight: bold; +} + +#page-blocks-community-communitycourse .hubcommenttext { + margin-bottom: 10px; +} + +#page-blocks-community-communitycourse .hubnoscriptcoursecomments { + margin-left: 5px; +} + +#page-blocks-community-communitycourse .yui3-overlay-loading { + /* Hide overlay markup while loading, if js is enabled */ + top: -1000em; + left: -1000em; + position: absolute; + z-index: 1000; +} + +#page-blocks-community-communitycourse .hubcoursecomments { + /* comment button */ + display: inline-block; + padding: 3px 3px 3px 3px; + color: white; + text-decoration: none; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + position: relative; + cursor: pointer; + background-color: #8b8989; + margin-left: 0; + font-size: 80%; + margin-top: 15px; +} + +#page-blocks-community-communitycourse .hubrateandcomment { + font-size: 80%; +} + +#page-blocks-community-communitycourse .nextlink { + text-align: center; + margin-top: 6px; +} + +#page-blocks-community-communitycourse .textinfo { + text-align: center; +} + +#ss-mask { + z-index: 10; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0.35; + filter: alpha(opacity=35); + background: #000; +} + +.hiddenoverlay { + display: none; + text-align: center; +} + +.imagearrow { + font-size: 120%; + display: inline; + cursor: pointer; +} + +.imagetitle { + display: inline; + cursor: pointer; +} + +#page-blocks-community-communitycourse .moodle-dialogue-base .moodle-dialogue { + -moz-border-radius: 12px 12px 12px 12px; + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); + -webkit-border-radius: 12px 12px 12px 12px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); + border-width: 0 0 0 0; +} + +#page-blocks-community-communitycourse .moodle-dialogue-base .moodle-dialogue-wrap { + -moz-border-radius: 12px 12px 0 0; + -webkit-border-radius: 12px 12px 0 0; + background-color: #fff; + border: 1px solid #555; +} + +#page-blocks-community-communitycourse .moodle-dialogue-base .moodle-dialogue-hd { + -moz-border-radius: 12px 12px 0 0; + -webkit-border-radius: 12px 12px 0 0; + background-color: #f6f6f6; + border: 1px solid #ccc; + overflow: auto; + padding: 7px 6px; +} + +#page-blocks-community-communitycourse .moodle-dialogue-base .moodle-dialogue-bd { + padding: 0; + margin-bottom: -5px; +} + +#page-blocks-community-communitycourse .moodle-dialogue-base .closebutton { + margin-top: 4px; + margin-right: 4px; +} diff --git a/community/tests/privacy_test.php b/community/tests/privacy_test.php new file mode 100644 index 0000000..48f672f --- /dev/null +++ b/community/tests/privacy_test.php @@ -0,0 +1,267 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_community implementation of the privacy API. + * + * @package block_community + * @category test + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \block_community\privacy\provider; + +/** + * Unit tests for the block_community implementation of the privacy API. + * + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_community_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Overriding setUp() function to always reset after tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('block_community'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(1, $itemcollection); + + $table = reset($itemcollection); + $this->assertEquals('block_community', $table->get_name()); + + $privacyfields = $table->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('coursename', $privacyfields); + $this->assertArrayHasKey('coursedescription', $privacyfields); + $this->assertArrayHasKey('courseurl', $privacyfields); + $this->assertArrayHasKey('imageurl', $privacyfields); + + $this->assertEquals('privacy:metadata:block_community', $table->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + global $DB; + + // Test setup. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add two community links for the User. + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - 1', + 'coursedescription' => 'Dummy Community Course Description - 1', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-1', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - 2', + 'coursedescription' => 'Dummy Community Course Description - 2', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-2', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($teacher->id, $context->instanceid); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_user_data() { + global $DB; + + // Test setup. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add 3 community links for the User. + $nocommunities = 3; + for ($c = 0; $c < $nocommunities; $c++) { + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - ' . $c, + 'coursedescription' => 'Dummy Community Course Description - ' . $c, + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c, + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + } + + // Test the created block_community records matches the test number of communities specified. + $communities = $DB->get_records('block_community', ['userid' => $teacher->id]); + $this->assertCount($nocommunities, $communities); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($teacher->id, $context->instanceid); + + $approvedcontextlist = new approved_contextlist($teacher, 'block_community', $contextlist->get_contextids()); + + // Retrieve Calendar Event and Subscriptions data only for this user. + provider::export_user_data($approvedcontextlist); + + // Test the block_community data is exported at the User context level. + $user = $approvedcontextlist->get_user(); + $contextuser = context_user::instance($user->id); + $writer = writer::with_context($contextuser); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + // Test setup. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add a community link for the User. + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name', + 'coursedescription' => 'Dummy Community Course Description', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($teacher->id, $context->instanceid); + + // Test delete all users content by context. + provider::delete_data_for_all_users_in_context($context); + $blockcommunity = $DB->get_records('block_community', ['userid' => $teacher->id]); + $this->assertCount(0, $blockcommunity); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + // Test setup. + $teacher1 = $this->getDataGenerator()->create_user(); + $teacher2 = $this->getDataGenerator()->create_user(); + $this->setUser($teacher1); + + // Add 3 community links for Teacher 1. + $nocommunities = 3; + for ($c = 0; $c < $nocommunities; $c++) { + $community = (object)[ + 'userid' => $teacher1->id, + 'coursename' => 'Dummy Community Course Name - ' . $c, + 'coursedescription' => 'Dummy Community Course Description - ' . $c, + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c, + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + } + + // Add 1 community link for Teacher 2. + $community = (object)[ + 'userid' => $teacher2->id, + 'coursename' => 'Dummy Community Course Name - Blah', + 'coursedescription' => 'Dummy Community Course Description - Blah', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-Blah', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the created block_community records for Teacher 1 equals test number of communities specified. + $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]); + $this->assertCount($nocommunities, $communities); + + // Test the created block_community records for Teacher 2 equals 1. + $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]); + $this->assertCount(1, $communities); + + // Test the deletion of block_community records for Teacher 1 results in zero records. + $contextlist = provider::get_contexts_for_userid($teacher1->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($teacher1->id, $context->instanceid); + + $approvedcontextlist = new approved_contextlist($teacher1, 'block_community', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]); + $this->assertCount(0, $communities); + + + // Test that Teacher 2's single block_community record still exists. + $contextlist = provider::get_contexts_for_userid($teacher2->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($teacher2->id, $context->instanceid); + + $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]); + $this->assertCount(1, $communities); + } + +} diff --git a/community/version.php b/community/version.php new file mode 100644 index 0000000..9bc9957 --- /dev/null +++ b/community/version.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/>. + +/** + * Version details + * + * @package block_community + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_community'; // Full name of the plugin (used for diagnostics) diff --git a/community/yui/comments/comments.js b/community/yui/comments/comments.js new file mode 100644 index 0000000..e720c18 --- /dev/null +++ b/community/yui/comments/comments.js @@ -0,0 +1,97 @@ +YUI.add('moodle-block_community-comments', function(Y) { + + var COMMENTSNAME = 'blocks_community_comments'; + + var COMMENTS = function() { + COMMENTS.superclass.constructor.apply(this, arguments); + }; + + Y.extend(COMMENTS, Y.Base, { + + event:null, + panelevent: null, + panels: [], //all the comment boxes + + initializer : function(params) { + + //attach a show event on the div with id = comments + for (var i=0;i<this.get('commentids').length;i++) + { + var commentid = this.get('commentids')[i]; + this.panels[commentid] = new M.core.dialogue({ + headerContent:Y.Node.create('<h1>') + .append(Y.one('#commentoverlay-'+commentid+' .commenttitle').get('innerHTML')), + bodyContent:Y.one('#commentoverlay-'+commentid).get('innerHTML'), + visible: false, //by default it is not displayed + modal: false, + zIndex:100, + closeButtonTitle: this.get('closeButtonTitle') + }); + + this.panels[commentid].get('contentBox').one('.commenttitle').remove(); + this.panels[commentid].render(); + this.panels[commentid].hide(); + + Y.one('#comments-'+commentid).on('click', this.show, this, commentid); + } + + }, + + show : function (e, commentid) { + + // Hide all panels. + for (var i=0;i<this.get('commentids').length;i++) + { + this.hide(e, this.get('commentids')[i]); + } + + this.panels[commentid].show(); //show the panel + + e.halt(); // we are going to attach a new 'hide panel' event to the body, + // because javascript always propagate event to parent tag, + // we need to tell Yahoo to stop to call the event on parent tag + // otherwise the hide event will be call right away. + + // We add a new event on the body in order to hide the panel for the next click. + this.event = Y.one(document.body).on('click', this.hide, this, commentid); + // We add a new event on the panel in order to hide the panel for the next click (touch device). + this.panelevent = Y.one("#commentoverlay-"+commentid).on('click', this.hide, this, commentid); + + // Focus on the close button + this.panels[commentid].get('buttons').header[0].focus(); + }, + + hide : function (e, commentid) { + this.panels[commentid].hide(); //hide the panel + if (this.event != null) { + this.event.detach(); //we need to detach the body hide event + //Note: it would work without but create js warning everytime + //we click on the body + } + if (this.panelevent != null) { + this.panelevent.detach(); //we need to detach the panel hide event + //Note: it would work without but create js warning everytime + //we click on the body + } + + } + + }, { + NAME : COMMENTSNAME, + ATTRS : { + commentids: {}, + closeButtonTitle : { + validator : Y.Lang.isString, + value : 'Close' + } + } + }); + + M.blocks_community = M.blocks_community || {}; + M.blocks_community.init_comments = function(params) { + return new COMMENTS(params); + } + +}, '@VERSION@', { + requires:['base', 'moodle-core-notification'] +}); diff --git a/community/yui/imagegallery/imagegallery.js b/community/yui/imagegallery/imagegallery.js new file mode 100644 index 0000000..b48a204 --- /dev/null +++ b/community/yui/imagegallery/imagegallery.js @@ -0,0 +1,206 @@ +YUI.add('moodle-block_community-imagegallery', function(Y) { + + var IMAGEGALLERYNAME = 'blocks_community_imagegallery'; + + var IMAGEGALLERY = function() { + IMAGEGALLERY.superclass.constructor.apply(this, arguments); + }; + + Y.extend(IMAGEGALLERY, Y.Base, { + + event:null, + previousevent:null, + nextevent:null, + panelevent:null, + panel:null, //all the images boxes + imageidnumbers: [], + imageloadingevent: null, + loadingimage: null, + + initializer : function(params) { + + //create the loading image + var objBody = Y.one(document.body); + this.loadingimage = Y.Node.create('<div id="hubloadingimage" class="hiddenoverlay">' + +'<img src=\'' + M.cfg.wwwroot +'/pix/i/loading.gif\'>' + +'</div>'); + objBody.append(this.loadingimage); + + // Create the div for panel. + var objBody = Y.one(document.body); + var paneltitle = Y.Node.create('<div id="imagetitleoverlay" class="hiddenoverlay"></div>'); + objBody.append(paneltitle); + var panel = Y.Node.create('<div id="imageoverlay" class="hiddenoverlay"></div>'); + objBody.append(panel); + + /// Create the panel. + this.panel = new M.core.dialogue({ + headerContent:Y.one('#imagetitleoverlay').get('innerHTML'), + bodyContent:Y.one('#imageoverlay').get('innerHTML'), + visible: false, //by default it is not displayed + modal: false, + zIndex:100 + }); + + this.panel.render(); + this.panel.hide(); + + //attach a show event on the image divs (<tag id='image-X'>) + for (var i=0;i<this.get('imageids').length;i++) + { + var imageid = this.get('imageids')[i]; + this.imageidnumbers[imageid] = this.get('imagenumbers')[i]; + Y.one('#image-'+imageid).on('click', this.show, this, imageid, 1); + } + + }, + + show : function (e, imageid, screennumber) { + + if (this.imageloadingevent != null) { + this.imageloadingevent.detach(); + } + + var url = this.get('huburl') + "/local/hub/webservice/download.php?courseid=" + + imageid + "&filetype=screenshot&imagewidth=original&screenshotnumber=" + screennumber; + + /// set the mask + if (this.get('maskNode')) { + this.get('maskNode').remove(); + } + var objBody = Y.one(document.body); + var mask = Y.Node.create('<div id="ss-mask"><!-- --></div>'); + objBody.prepend(mask); + this.set('maskNode', Y.one('#ss-mask')); + + //display loading image + Y.one('#hubloadingimage').setStyle('display', 'block'); + Y.one('#hubloadingimage').setStyle("position", 'fixed'); + Y.one('#hubloadingimage').setStyle("top", '50%'); + Y.one('#hubloadingimage').setStyle("left", '50%'); + + var windowheight = e.target.get('winHeight'); + var windowwidth = e.target.get('winWidth'); + + var maxheight = windowheight - 150; + + //load the title + link to next image + var paneltitle = Y.one('#imagetitleoverlay'); + var previousimagelink = "<div id=\"previousarrow\" class=\"imagearrow\">←</div>"; + var nextimagelink = "<div id=\"nextarrow\" class=\"imagearrow\">→</div>"; + + // Need to load the images in the panel. + var panel = Y.one('#imageoverlay'); + panel.setContent(''); + + panel.append(Y.Node.create('<div style="text-align:center"><img id=\"imagetodisplay\" src="' + url + + '" style="max-height:' + maxheight + 'px;"></div>')); + this.panel.destroy(); + this.panel = new M.core.dialogue({ + headerContent:previousimagelink + '<div id=\"imagenumber\" class=\"imagetitle\"><h1> Image ' + + screennumber + ' / ' + this.imageidnumbers[imageid] + ' </h1></div>' + nextimagelink, + bodyContent:Y.one('#imageoverlay').get('innerHTML'), + visible: false, //by default it is not displayed + modal: false, + zIndex:100, + closeButtonTitle: this.get('closeButtonTitle') + }); + this.panel.render(); + this.panel.hide(); //show the panel + this.panel.set("centered", true); + + e.halt(); // we are going to attach a new 'hide panel' event to the body, + // because javascript always propagate event to parent tag, + // we need to tell Yahoo to stop to call the event on parent tag + // otherwise the hide event will be call right away. + + //once the image is loaded, update display + this.imageloadingevent = Y.one('#imagetodisplay').on('load', function(e, url){ + //hide the loading image + Y.one('#hubloadingimage').setStyle('display', 'none'); + + //display the screenshot + var screenshot = new Image(); + screenshot.src = url; + + var panelwidth = windowwidth - 100; + if(panelwidth > screenshot.width) { + panelwidth = screenshot.width; + } + + this.panel.set('width', panelwidth); + this.panel.set("centered", true); + this.panel.show(); + + // Focus on the close button + this.panel.get('buttons').header[0].focus(); + + }, this, url); + + var previousnumber = screennumber - 1; + var nextnumber = screennumber + 1; + if (previousnumber == 0) { + previousnumber = this.imageidnumbers[imageid]; + } + if (nextnumber > this.imageidnumbers[imageid]) { + nextnumber = 1; + } + + Y.one('#previousarrow').on('click', this.show, this, imageid, previousnumber); + Y.one('#nextarrow').on('click', this.show, this, imageid, nextnumber); + Y.one('#imagenumber').on('click', this.show, this, imageid, nextnumber); + + // We add a new event on the body in order to hide the panel for the next click. + this.event = Y.one(document.body).on('click', this.hide, this); + // We add a new event on the panel in order to hide the panel for the next click (touch device). + this.panelevent = Y.one("#imageoverlay").on('click', this.hide, this); + + this.panel.on('visibleChange',function(e){ + if(e.newVal == 0){ + this.get('maskNode').remove() + } + }, this); + }, + + hide : function (e) { + + // remove the mask + this.get('maskNode').remove(); + + //hide the loading image + Y.one('#hubloadingimage').setStyle('display', 'none'); + + this.panel.hide(); //hide the panel + if (this.event != null) { + this.event.detach(); //we need to detach the body hide event + //Note: it would work without but create js warning everytime + //we click on the body + } + if (this.panelevent != null) { + this.panelevent.detach(); //we need to detach the panel hide event + //Note: it would work without but create js warning everytime + //we click on the body + } + } + + }, { + NAME : IMAGEGALLERYNAME, + ATTRS : { + imageids: {}, + imagenumbers: {}, + huburl: {}, + closeButtonTitle : { + validator : Y.Lang.isString, + value : 'Close' + } + } + }); + + M.blocks_community = M.blocks_community || {}; + M.blocks_community.init_imagegallery = function(params) { + return new IMAGEGALLERY(params); + } + +}, '@VERSION@', { + requires:['base','node', 'moodle-core-notification'] +}); diff --git a/completionstatus/block_completionstatus.php b/completionstatus/block_completionstatus.php new file mode 100644 index 0000000..b8627d9 --- /dev/null +++ b/completionstatus/block_completionstatus.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/>. + +/** + * Block for displayed logged in user's course completion status + * + * @package block_completionstatus + * @copyright 2009-2012 Catalyst IT Ltd + * @author Aaron Barnes <aaronb@catalyst.net.nz> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/completionlib.php"); + +/** + * Course completion status. + * Displays overall, and individual criteria status for logged in user. + */ +class block_completionstatus extends block_base { + + public function init() { + $this->title = get_string('pluginname', 'block_completionstatus'); + } + + public function applicable_formats() { + return array('course' => true); + } + + public function get_content() { + global $USER; + + $rows = array(); + $srows = array(); + $prows = array(); + // If content is cached. + if ($this->content !== null) { + return $this->content; + } + + $course = $this->page->course; + $context = context_course::instance($course->id); + + // Create empty content. + $this->content = new stdClass(); + $this->content->text = ''; + $this->content->footer = ''; + + // Can edit settings? + $can_edit = has_capability('moodle/course:update', $context); + + // Get course completion data. + $info = new completion_info($course); + + // Don't display if completion isn't enabled! + if (!completion_info::is_enabled_for_site()) { + if ($can_edit) { + $this->content->text .= get_string('completionnotenabledforsite', 'completion'); + } + return $this->content; + + } else if (!$info->is_enabled()) { + if ($can_edit) { + $this->content->text .= get_string('completionnotenabledforcourse', 'completion'); + } + return $this->content; + } + + // Load criteria to display. + $completions = $info->get_completions($USER->id); + + // Check if this course has any criteria. + if (empty($completions)) { + if ($can_edit) { + $this->content->text .= get_string('nocriteriaset', 'completion'); + } + return $this->content; + } + + // Check this user is enroled. + if ($info->is_tracked_user($USER->id)) { + + // Generate markup for criteria statuses. + $data = ''; + + // For aggregating activity completion. + $activities = array(); + $activities_complete = 0; + + // For aggregating course prerequisites. + $prerequisites = array(); + $prerequisites_complete = 0; + + // Flag to set if current completion data is inconsistent with what is stored in the database. + $pending_update = false; + + // Loop through course criteria. + foreach ($completions as $completion) { + $criteria = $completion->get_criteria(); + $complete = $completion->is_complete(); + + if (!$pending_update && $criteria->is_pending($completion)) { + $pending_update = true; + } + + // Activities are a special case, so cache them and leave them till last. + if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { + $activities[$criteria->moduleinstance] = $complete; + + if ($complete) { + $activities_complete++; + } + + continue; + } + + // Prerequisites are also a special case, so cache them and leave them till last. + if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) { + $prerequisites[$criteria->courseinstance] = $complete; + + if ($complete) { + $prerequisites_complete++; + } + + continue; + } + $row = new html_table_row(); + $row->cells[0] = new html_table_cell($criteria->get_title()); + $row->cells[1] = new html_table_cell($completion->get_status()); + $row->cells[1]->style = 'text-align: right;'; + $srows[] = $row; + } + + // Aggregate activities. + if (!empty($activities)) { + $a = new stdClass(); + $a->first = $activities_complete; + $a->second = count($activities); + + $row = new html_table_row(); + $row->cells[0] = new html_table_cell(get_string('activitiescompleted', 'completion')); + $row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); + $row->cells[1]->style = 'text-align: right;'; + $srows[] = $row; + } + + // Aggregate prerequisites. + if (!empty($prerequisites)) { + $a = new stdClass(); + $a->first = $prerequisites_complete; + $a->second = count($prerequisites); + + $row = new html_table_row(); + $row->cells[0] = new html_table_cell(get_string('dependenciescompleted', 'completion')); + $row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); + $row->cells[1]->style = 'text-align: right;'; + $prows[] = $row; + + $srows = array_merge($prows, $srows); + } + + // Display completion status. + $table = new html_table(); + $table->width = '100%'; + $table->attributes = array('style'=>'font-size: 90%;', 'class'=>''); + + $row = new html_table_row(); + $content = html_writer::tag('b', get_string('status').': '); + + // Is course complete? + $coursecomplete = $info->is_course_complete($USER->id); + + // Load course completion. + $params = array( + 'userid' => $USER->id, + 'course' => $course->id + ); + $ccompletion = new completion_completion($params); + + // Has this user completed any criteria? + $criteriacomplete = $info->count_course_user_data($USER->id); + + if ($pending_update) { + $content .= html_writer::tag('i', get_string('pending', 'completion')); + } else if ($coursecomplete) { + $content .= get_string('complete'); + } else if (!$criteriacomplete && !$ccompletion->timestarted) { + $content .= html_writer::tag('i', get_string('notyetstarted', 'completion')); + } else { + $content .= html_writer::tag('i', get_string('inprogress', 'completion')); + } + + $row->cells[0] = new html_table_cell($content); + $row->cells[0]->colspan = '2'; + + $rows[] = $row; + $row = new html_table_row(); + $content = ""; + // Get overall aggregation method. + $overall = $info->get_aggregation_method(); + if ($overall == COMPLETION_AGGREGATION_ALL) { + $content .= get_string('criteriarequiredall', 'completion'); + } else { + $content .= get_string('criteriarequiredany', 'completion'); + } + $content .= ':'; + $row->cells[0] = new html_table_cell($content); + $row->cells[0]->colspan = '2'; + $rows[] = $row; + + $row = new html_table_row(); + $row->cells[0] = new html_table_cell(html_writer::tag('b', get_string('requiredcriteria', 'completion'))); + $row->cells[1] = new html_table_cell(html_writer::tag('b', get_string('status'))); + $row->cells[1]->style = 'text-align: right;'; + $rows[] = $row; + + // Array merge $rows and $data here. + $rows = array_merge($rows, $srows); + + $table->data = $rows; + $this->content->text .= html_writer::table($table); + + // Display link to detailed view. + $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id)); + $this->content->footer .= html_writer::link($details, get_string('moredetails', 'completion')); + } else { + // If user is not enrolled, show error. + $this->content->text = get_string('nottracked', 'completion'); + } + + if (has_capability('report/completion:view', $context)) { + $report = new moodle_url('/report/completion/index.php', array('course' => $course->id)); + if (empty($this->content->footer)) { + $this->content->footer = ''; + } + $this->content->footer .= html_writer::empty_tag('br'); + $this->content->footer .= html_writer::link($report, get_string('viewcoursereport', 'completion')); + } + + return $this->content; + } +} diff --git a/completionstatus/classes/privacy/provider.php b/completionstatus/classes/privacy/provider.php new file mode 100644 index 0000000..97fd398 --- /dev/null +++ b/completionstatus/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_completionstatus. + * + * @package block_completionstatus + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_completionstatus\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_completionstatus implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/completionstatus/db/access.php b/completionstatus/db/access.php new file mode 100644 index 0000000..8d38ec8 --- /dev/null +++ b/completionstatus/db/access.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/>. + +/** + * Completion status block caps. + * + * @package block_completionstatus + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/completionstatus: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/completionstatus/db/upgrade.php b/completionstatus/db/upgrade.php new file mode 100644 index 0000000..f0837fc --- /dev/null +++ b/completionstatus/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the completion status block + * + * 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. + * + * @since Moodle 2.0 + * @package block_completionstatus + * @copyright 2012 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Handles upgrading instances of this block. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_completionstatus_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/completionstatus/details.php b/completionstatus/details.php new file mode 100644 index 0000000..33bbe77 --- /dev/null +++ b/completionstatus/details.php @@ -0,0 +1,263 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 for displaying logged in user's course completion status + * + * @package block_completionstatus + * @copyright 2009-2012 Catalyst IT Ltd + * @author Aaron Barnes <aaronb@catalyst.net.nz> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__.'/../../config.php'); +require_once("{$CFG->libdir}/completionlib.php"); + +// Load data. +$id = required_param('course', PARAM_INT); +$userid = optional_param('user', 0, PARAM_INT); + +// Load course. +$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); + +// Load user. +if ($userid) { + $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); +} else { + $user = $USER; +} + +// Check permissions. +require_login(); + +if (!completion_can_view_data($user->id, $course)) { + print_error('cannotviewreport'); +} + +// Load completion data. +$info = new completion_info($course); + +$returnurl = new moodle_url('/course/view.php', array('id' => $id)); + +// Don't display if completion isn't enabled. +if (!$info->is_enabled()) { + print_error('completionnotenabled', 'completion', $returnurl); +} + +// Check this user is enroled. +if (!$info->is_tracked_user($user->id)) { + if ($USER->id == $user->id) { + print_error('notenroled', 'completion', $returnurl); + } else { + print_error('usernotenroled', 'completion', $returnurl); + } +} + +// Display page. + +$PAGE->set_context(context_course::instance($course->id)); + +// Print header. +$page = get_string('completionprogressdetails', 'block_completionstatus'); +$title = format_string($course->fullname) . ': ' . $page; + +$PAGE->navbar->add($page); +$PAGE->set_pagelayout('report'); +$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id)); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_heading($title); +echo $OUTPUT->header(); + + +// Display completion status. +echo html_writer::start_tag('table', array('class' => 'generalbox boxaligncenter')); +echo html_writer::start_tag('tbody'); + +// If not display logged in user, show user name. +if ($USER->id != $user->id) { + echo html_writer::start_tag('tr'); + echo html_writer::start_tag('td', array('colspan' => '2')); + echo html_writer::tag('b', get_string('showinguser', 'completion') . ' '); + $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); + echo html_writer::link($url, fullname($user)); + echo html_writer::end_tag('td'); + echo html_writer::end_tag('tr'); +} + +echo html_writer::start_tag('tr'); +echo html_writer::start_tag('td', array('colspan' => '2')); +echo html_writer::tag('b', get_string('status') . ' '); + +// Is course complete? +$coursecomplete = $info->is_course_complete($user->id); + +// Has this user completed any criteria? +$criteriacomplete = $info->count_course_user_data($user->id); + +// Load course completion. +$params = array( + 'userid' => $user->id, + 'course' => $course->id, +); +$ccompletion = new completion_completion($params); + +if ($coursecomplete) { + echo get_string('complete'); +} else if (!$criteriacomplete && !$ccompletion->timestarted) { + echo html_writer::tag('i', get_string('notyetstarted', 'completion')); +} else { + echo html_writer::tag('i', get_string('inprogress', 'completion')); +} + +echo html_writer::end_tag('td'); +echo html_writer::end_tag('tr'); + +// Load criteria to display. +$completions = $info->get_completions($user->id); + +// Check if this course has any criteria. +if (empty($completions)) { + echo html_writer::start_tag('tr'); + echo html_writer::start_tag('td', array('colspan' => '2')); + echo html_writer::start_tag('br'); + echo $OUTPUT->box(get_string('nocriteriaset', 'completion'), 'noticebox'); + echo html_writer::end_tag('td'); + echo html_writer::end_tag('tr'); + echo html_writer::end_tag('tbody'); + echo html_writer::end_tag('table'); +} else { + echo html_writer::start_tag('tr'); + echo html_writer::start_tag('td', array('colspan' => '2')); + echo html_writer::tag('b', get_string('required') . ' '); + + // Get overall aggregation method. + $overall = $info->get_aggregation_method(); + + if ($overall == COMPLETION_AGGREGATION_ALL) { + echo get_string('criteriarequiredall', 'completion'); + } else { + echo get_string('criteriarequiredany', 'completion'); + } + + echo html_writer::end_tag('td'); + echo html_writer::end_tag('tr'); + echo html_writer::end_tag('tbody'); + echo html_writer::end_tag('table'); + + // Generate markup for criteria statuses. + echo html_writer::start_tag('table', + array('class' => 'generalbox logtable boxaligncenter', 'id' => 'criteriastatus', 'width' => '100%')); + echo html_writer::start_tag('tbody'); + echo html_writer::start_tag('tr', array('class' => 'ccheader')); + echo html_writer::tag('th', get_string('criteriagroup', 'block_completionstatus'), array('class' => 'c0 header', 'scope' => 'col')); + echo html_writer::tag('th', get_string('criteria', 'completion'), array('class' => 'c1 header', 'scope' => 'col')); + echo html_writer::tag('th', get_string('requirement', 'block_completionstatus'), array('class' => 'c2 header', 'scope' => 'col')); + echo html_writer::tag('th', get_string('status'), array('class' => 'c3 header', 'scope' => 'col')); + echo html_writer::tag('th', get_string('complete'), array('class' => 'c4 header', 'scope' => 'col')); + echo html_writer::tag('th', get_string('completiondate', 'report_completion'), array('class' => 'c5 header', 'scope' => 'col')); + echo html_writer::end_tag('tr'); + + // Save row data. + $rows = array(); + + // Loop through course criteria. + foreach ($completions as $completion) { + $criteria = $completion->get_criteria(); + + $row = array(); + $row['type'] = $criteria->criteriatype; + $row['title'] = $criteria->get_title(); + $row['status'] = $completion->get_status(); + $row['complete'] = $completion->is_complete(); + $row['timecompleted'] = $completion->timecompleted; + $row['details'] = $criteria->get_details($completion); + $rows[] = $row; + } + + // Print table. + $last_type = ''; + $agg_type = false; + $oddeven = 0; + + foreach ($rows as $row) { + + echo html_writer::start_tag('tr', array('class' => 'r' . $oddeven)); + // Criteria group. + echo html_writer::start_tag('td', array('class' => 'cell c0')); + if ($last_type !== $row['details']['type']) { + $last_type = $row['details']['type']; + echo $last_type; + + // Reset agg type. + $agg_type = true; + } else { + // Display aggregation type. + if ($agg_type) { + $agg = $info->get_aggregation_method($row['type']); + echo '('. html_writer::start_tag('i'); + if ($agg == COMPLETION_AGGREGATION_ALL) { + echo core_text::strtolower(get_string('all', 'completion')); + } else { + echo core_text::strtolower(get_string('any', 'completion')); + } + + echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')'; + $agg_type = false; + } + } + echo html_writer::end_tag('td'); + + // Criteria title. + echo html_writer::start_tag('td', array('class' => 'cell c1')); + echo $row['details']['criteria']; + echo html_writer::end_tag('td'); + + // Requirement. + echo html_writer::start_tag('td', array('class' => 'cell c2')); + echo $row['details']['requirement']; + echo html_writer::end_tag('td'); + + // Status. + echo html_writer::start_tag('td', array('class' => 'cell c3')); + echo $row['details']['status']; + echo html_writer::end_tag('td'); + + // Is complete. + echo html_writer::start_tag('td', array('class' => 'cell c4')); + echo $row['complete'] ? get_string('yes') : get_string('no'); + echo html_writer::end_tag('td'); + + // Completion data. + echo html_writer::start_tag('td', array('class' => 'cell c5')); + if ($row['timecompleted']) { + echo userdate($row['timecompleted'], get_string('strftimedate', 'langconfig')); + } else { + echo '-'; + } + echo html_writer::end_tag('td'); + echo html_writer::end_tag('tr'); + // For row striping. + $oddeven = $oddeven ? 0 : 1; + } + + echo html_writer::end_tag('tbody'); + echo html_writer::end_tag('table'); +} +$courseurl = new moodle_url("/course/view.php", array('id' => $course->id)); +echo html_writer::start_tag('div', array('class' => 'buttons')); +echo $OUTPUT->single_button($courseurl, get_string('returntocourse', 'block_completionstatus'), 'get'); +echo html_writer::end_tag('div'); +echo $OUTPUT->footer(); diff --git a/completionstatus/lang/en/block_completionstatus.php b/completionstatus/lang/en/block_completionstatus.php new file mode 100644 index 0000000..dc93b6f --- /dev/null +++ b/completionstatus/lang/en/block_completionstatus.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/>. + +/** + * Strings for component 'block_completionstatus', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_completionstatus + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['completionprogressdetails'] = 'Completion progress details'; +$string['completionstatus:addinstance'] = 'Add a new course completion status block'; +$string['criteriagroup'] = 'Criteria group'; +$string['firstofsecond'] = '{$a->first} of {$a->second}'; +$string['pluginname'] = 'Course completion status'; +$string['requirement'] = 'Requirement'; +$string['returntocourse'] = 'Return to course'; +$string['privacy:metadata'] = 'The Course completion status block only shows information about course completion and does not store any data of its own.'; diff --git a/completionstatus/tests/behat/block_completionstatus.feature b/completionstatus/tests/behat/block_completionstatus.feature new file mode 100644 index 0000000..f5e24a1 --- /dev/null +++ b/completionstatus/tests/behat/block_completionstatus.feature @@ -0,0 +1,54 @@ +@block @block_completionstatus +Feature: Enable Block Completion in a course + In order to view the completion block in a course + As a teacher + I can add completion block to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + + Scenario: Add the block to a the course where completion is disabled + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I navigate to "Edit settings" node in "Course administration" + And I set the following fields to these values: + | Enable completion tracking | No | + And I press "Save and display" + When I add the "Course completion status" block + Then I should see "Completion is not enabled for this course" in the "Course completion status" "block" + + Scenario: Add the block to a the course where completion is not set + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Course completion status" block + Then I should see "No completion criteria set for this course" in the "Course completion status" "block" + + Scenario: Add the block to a course with criteria and view as an untracked role. + Given the following "activities" exist: + | activity | course | idnumber | name | intro | + | page | C1 | page1 | Test page name | Test page description | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + | Require view | 1 | + And I press "Save and return to course" + When I add the "Course completion status" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | Test page name | 1 | + And I press "Save changes" + Then I should see "You are currently not being tracked by completion in this course" in the "Course completion status" "block" diff --git a/completionstatus/tests/behat/block_completionstatus_activity_completion.feature b/completionstatus/tests/behat/block_completionstatus_activity_completion.feature new file mode 100644 index 0000000..d41fe98 --- /dev/null +++ b/completionstatus/tests/behat/block_completionstatus_activity_completion.feature @@ -0,0 +1,70 @@ +@block @block_completionstatus +Feature: Enable Block Completion in a course using activity completion + In order to view the completion block in a course + As a teacher + I can add completion block to a course and set up activity completion + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | course | idnumber | name | intro | + | page | C1 | page1 | Test page name | Test page description | + + Scenario: Add the block to a the course and add course completion items + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + | Require view | 1 | + And I press "Save and return to course" + And I add the "Course completion status" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | Test page name | 1 | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Status: Not yet started" in the "Course completion status" "block" + And I should see "0 of 1" in the "Activity completion" "table_row" + + Scenario: Add the block to a the course and add course completion items + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + | Require view | 1 | + And I press "Save and return to course" + And I add the "Course completion status" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | Test page name | 1 | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test page name" + And I am on "Course 1" course homepage + Then I should see "Status: Pending" in the "Course completion status" "block" + And I should see "0 of 1" in the "Activity completion" "table_row" + And I trigger cron + And I am on "Course 1" course homepage + And I should see "1 of 1" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "Yes" in the "Activity completion" "table_row" diff --git a/completionstatus/tests/behat/block_completionstatus_manual_other.feature b/completionstatus/tests/behat/block_completionstatus_manual_other.feature new file mode 100644 index 0000000..c021914 --- /dev/null +++ b/completionstatus/tests/behat/block_completionstatus_manual_other.feature @@ -0,0 +1,103 @@ +@block @block_completionstatus +Feature: Enable Block Completion in a course using manual completion by others + In order to view the completion block in a course + As a teacher + I can add completion block to a course and set up manual completion by others + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | teacher2 | Teacher | 2 | teacher1@example.com | T2 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | teacher | + | student1 | C1 | student | + + Scenario: Add the block to a the course and mark a student complete. + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Course completion status" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | Teacher | 1 | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Status: Not yet started" in the "Course completion status" "block" + And I should see "No" in the "Teacher" "table_row" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Course completion" node in "Course administration > Reports" + And I follow "Click to mark user complete" + # Running completion task just after clicking sometimes fail, as record + # should be created before the task runs. + And I wait "1" seconds + And I run the scheduled task "core\task\completion_regular_task" + And I am on site homepage + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Status: Complete" in the "Course completion status" "block" + And I should see "Yes" in the "Teacher" "table_row" + And I follow "More details" + And I should see "Yes" in the "Marked complete by Teacher" "table_row" + + Scenario: Add the block to a the course and require multiple roles to mark a student complete. + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Course completion status" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | Teacher | 1 | + | Non-editing teacher | 1 | + | id_role_aggregation | ALL selected roles to mark when the condition is met | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Status: Not yet started" in the "Course completion status" "block" + And I should see "No" in the "Teacher" "table_row" + And I should see "No" in the "Non-editing teacher" "table_row" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Course completion" node in "Course administration > Reports" + And I follow "Click to mark user complete" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Status: In progress" in the "Course completion status" "block" + And I should see "Yes" in the "Teacher" "table_row" + And I should see "No" in the "Non-editing teacher" "table_row" + And I follow "More details" + And I should see "Yes" in the "Marked complete by Teacher" "table_row" + And I should see "No" in the "Marked complete by Non-editing teacher" "table_row" + And I log out + And I log in as "teacher2" + And I am on "Course 1" course homepage + And I navigate to "Course completion" node in "Course administration > Reports" + And I follow "Click to mark user complete" + # Running completion task just after clicking sometimes fail, as record + # should be created before the task runs. + And I wait "1" seconds + And I run the scheduled task "core\task\completion_regular_task" + And I am on site homepage + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Status: Complete" in the "Course completion status" "block" + And I should see "Yes" in the "Teacher" "table_row" + And I should see "Yes" in the "Non-editing teacher" "table_row" + And I follow "More details" + And I should see "Yes" in the "Marked complete by Teacher" "table_row" + And I should see "Yes" in the "Marked complete by Non-editing teacher" "table_row" diff --git a/completionstatus/tests/behat/block_completionstatus_manual_self.feature b/completionstatus/tests/behat/block_completionstatus_manual_self.feature new file mode 100644 index 0000000..d9b73c5 --- /dev/null +++ b/completionstatus/tests/behat/block_completionstatus_manual_self.feature @@ -0,0 +1,45 @@ +@block @block_completionstatus @block_selfcompletion +Feature: Enable Block Completion in a course using manual self completion + In order to view the completion block in a course + As a teacher + I can add completion block to a course and set up manual self completion + + Scenario: Add the block to a the course and manually complete the course + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Course completion status" block + And I add the "Self completion" block + And I navigate to "Course completion" node in "Course administration" + And I expand all fieldsets + And I set the following fields to these values: + | id_criteria_self | 1 | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Status: Not yet started" in the "Course completion status" "block" + And I should see "No" in the "Self completion" "table_row" + And I follow "Complete course" + And I should see "Confirm self completion" + And I press "Yes" + And I should see "Status: In progress" in the "Course completion status" "block" + # Running completion task just after clicking sometimes fail, as record + # should be created before the task runs. + And I wait "1" seconds + And I run the scheduled task "core\task\completion_regular_task" + And I am on "Course 1" course homepage + Then I should see "Status: Complete" in the "Course completion status" "block" + And I should see "Yes" in the "Self completion" "table_row" + And I follow "More details" + And I should see "Yes" in the "Self completion" "table_row" diff --git a/completionstatus/version.php b/completionstatus/version.php new file mode 100644 index 0000000..1009943 --- /dev/null +++ b/completionstatus/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 info + * + * @package block_completionstatus + * @copyright 2009 Catalyst IT Ltd + * @author Aaron Barnes <aaronb@catalyst.net.nz> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2018050800; // Requires this Moodle version. +$plugin->component = 'block_completionstatus'; +$plugin->dependencies = array('report_completion' => 2018050800); diff --git a/course_list/block_course_list.php b/course_list/block_course_list.php new file mode 100644 index 0000000..10cbf23 --- /dev/null +++ b/course_list/block_course_list.php @@ -0,0 +1,177 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 list block. + * + * @package block_course_list + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +include_once($CFG->dirroot . '/course/lib.php'); +include_once($CFG->libdir . '/coursecatlib.php'); + +class block_course_list extends block_list { + function init() { + $this->title = get_string('pluginname', 'block_course_list'); + } + + function has_config() { + return true; + } + + function get_content() { + global $CFG, $USER, $DB, $OUTPUT; + + if($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + $icon = $OUTPUT->pix_icon('i/course', get_string('course')); + + $adminseesall = true; + if (isset($CFG->block_course_list_adminview)) { + if ( $CFG->block_course_list_adminview == 'own'){ + $adminseesall = false; + } + } + + if (empty($CFG->disablemycourses) and isloggedin() and !isguestuser() and + !(has_capability('moodle/course:update', context_system::instance()) and $adminseesall)) { // Just print My Courses + if ($courses = enrol_get_my_courses()) { + foreach ($courses as $course) { + $coursecontext = context_course::instance($course->id); + $linkcss = $course->visible ? "" : " class=\"dimmed\" "; + $this->content->items[]="<a $linkcss title=\"" . format_string($course->shortname, true, array('context' => $coursecontext)) . "\" ". + "href=\"$CFG->wwwroot/course/view.php?id=$course->id\">".$icon.format_string(get_course_display_name_for_list($course)). "</a>"; + } + $this->title = get_string('mycourses'); + /// If we can update any course of the view all isn't hidden, show the view all courses link + if (has_capability('moodle/course:update', context_system::instance()) || empty($CFG->block_course_list_hideallcourseslink)) { + $this->content->footer = "<a href=\"$CFG->wwwroot/course/index.php\">".get_string("fulllistofcourses")."</a> ..."; + } + } + $this->get_remote_courses(); + if ($this->content->items) { // make sure we don't return an empty list + return $this->content; + } + } + + $categories = coursecat::get(0)->get_children(); // Parent = 0 ie top-level categories only + if ($categories) { //Check we have categories + if (count($categories) > 1 || (count($categories) == 1 && $DB->count_records('course') > 200)) { // Just print top level category links + foreach ($categories as $category) { + $categoryname = $category->get_formatted_name(); + $linkcss = $category->visible ? "" : " class=\"dimmed\" "; + $this->content->items[]="<a $linkcss href=\"$CFG->wwwroot/course/index.php?categoryid=$category->id\">".$icon . $categoryname . "</a>"; + } + /// If we can update any course of the view all isn't hidden, show the view all courses link + if (has_capability('moodle/course:update', context_system::instance()) || empty($CFG->block_course_list_hideallcourseslink)) { + $this->content->footer .= "<a href=\"$CFG->wwwroot/course/index.php\">".get_string('fulllistofcourses').'</a> ...'; + } + $this->title = get_string('categories'); + } else { // Just print course names of single category + $category = array_shift($categories); + $courses = get_courses($category->id); + + if ($courses) { + foreach ($courses as $course) { + $coursecontext = context_course::instance($course->id); + $linkcss = $course->visible ? "" : " class=\"dimmed\" "; + + $this->content->items[]="<a $linkcss title=\"" + . format_string($course->shortname, true, array('context' => $coursecontext))."\" ". + "href=\"$CFG->wwwroot/course/view.php?id=$course->id\">" + .$icon. format_string(get_course_display_name_for_list($course), true, array('context' => context_course::instance($course->id))) . "</a>"; + } + /// If we can update any course of the view all isn't hidden, show the view all courses link + if (has_capability('moodle/course:update', context_system::instance()) || empty($CFG->block_course_list_hideallcourseslink)) { + $this->content->footer .= "<a href=\"$CFG->wwwroot/course/index.php\">".get_string('fulllistofcourses').'</a> ...'; + } + $this->get_remote_courses(); + } else { + + $this->content->icons[] = ''; + $this->content->items[] = get_string('nocoursesyet'); + if (has_capability('moodle/course:create', context_coursecat::instance($category->id))) { + $this->content->footer = '<a href="'.$CFG->wwwroot.'/course/edit.php?category='.$category->id.'">'.get_string("addnewcourse").'</a> ...'; + } + $this->get_remote_courses(); + } + $this->title = get_string('courses'); + } + } + + return $this->content; + } + + function get_remote_courses() { + global $CFG, $USER, $OUTPUT; + + if (!is_enabled_auth('mnet')) { + // no need to query anything remote related + return; + } + + $icon = $OUTPUT->pix_icon('i/mnethost', get_string('host', 'mnet')); + + // shortcut - the rest is only for logged in users! + if (!isloggedin() || isguestuser()) { + return false; + } + + if ($courses = get_my_remotecourses()) { + $this->content->items[] = get_string('remotecourses','mnet'); + $this->content->icons[] = ''; + foreach ($courses as $course) { + $this->content->items[]="<a title=\"" . format_string($course->shortname, true) . "\" ". + "href=\"{$CFG->wwwroot}/auth/mnet/jump.php?hostid={$course->hostid}&wantsurl=/course/view.php?id={$course->remoteid}\">" + .$icon. format_string(get_course_display_name_for_list($course)) . "</a>"; + } + // if we listed courses, we are done + return true; + } + + if ($hosts = get_my_remotehosts()) { + $this->content->items[] = get_string('remotehosts', 'mnet'); + $this->content->icons[] = ''; + foreach($USER->mnet_foreign_host_array as $somehost) { + $this->content->items[] = $somehost['count'].get_string('courseson','mnet').'<a title="'.$somehost['name'].'" href="'.$somehost['url'].'">'.$icon.$somehost['name'].'</a>'; + } + // if we listed hosts, done + return true; + } + + return false; + } + + /** + * Returns the role that best describes the course list block. + * + * @return string + */ + public function get_aria_role() { + return 'navigation'; + } +} + + diff --git a/course_list/classes/privacy/provider.php b/course_list/classes/privacy/provider.php new file mode 100644 index 0000000..37066fd --- /dev/null +++ b/course_list/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_course_list. + * + * @package block_course_list + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_course_list\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_course_list implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/course_list/db/access.php b/course_list/db/access.php new file mode 100644 index 0000000..5d594fe --- /dev/null +++ b/course_list/db/access.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/>. + +/** + * Course list block caps. + * + * @package block_course_list + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/course_list:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/course_list: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/course_list/lang/en/block_course_list.php b/course_list/lang/en/block_course_list.php new file mode 100644 index 0000000..1cd9244 --- /dev/null +++ b/course_list/lang/en/block_course_list.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/>. + +/** + * Strings for component 'block_course_list', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_course_list + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['adminview'] = 'Admin view'; +$string['allcourses'] = 'Admin user sees all courses'; +$string['configadminview'] = 'What should the admin see in the course list block?'; +$string['confighideallcourseslink'] = 'Remove the \'All courses\' link under the list of courses. (This setting does not affect the admin view.)'; +$string['course_list:addinstance'] = 'Add a new courses block'; +$string['course_list:myaddinstance'] = 'Add a new courses block to Dashboard'; +$string['hideallcourseslink'] = 'Hide \'All courses\' link'; +$string['owncourses'] = 'Admin user sees own courses'; +$string['pluginname'] = 'Courses'; +$string['privacy:metadata'] = 'The Courses block only shows data about courses and does not store any data itself.'; diff --git a/course_list/settings.php b/course_list/settings.php new file mode 100644 index 0000000..0d8a9ef --- /dev/null +++ b/course_list/settings.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/>. + +/** + * Course list block settings + * + * @package block_course_list + * @copyright 2007 Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $options = array('all'=>get_string('allcourses', 'block_course_list'), 'own'=>get_string('owncourses', 'block_course_list')); + + $settings->add(new admin_setting_configselect('block_course_list_adminview', get_string('adminview', 'block_course_list'), + get_string('configadminview', 'block_course_list'), 'all', $options)); + + $settings->add(new admin_setting_configcheckbox('block_course_list_hideallcourseslink', get_string('hideallcourseslink', 'block_course_list'), + get_string('confighideallcourseslink', 'block_course_list'), 0)); +} + + diff --git a/course_list/styles.css b/course_list/styles.css new file mode 100644 index 0000000..b94fe89 --- /dev/null +++ b/course_list/styles.css @@ -0,0 +1,7 @@ +.block_course_list .footer { + margin-top: 5px; +} + +.block_course_list .content li { + margin-bottom: .3em; +} diff --git a/course_list/tests/behat/block_course_list_category.feature b/course_list/tests/behat/block_course_list_category.feature new file mode 100644 index 0000000..3c03721 --- /dev/null +++ b/course_list/tests/behat/block_course_list_category.feature @@ -0,0 +1,78 @@ +@block @block_course_list +Feature: Enable the course_list block on a category page and view it's contents + In order to enable the course list block on a category page + As an admin + I can add the course list block to a category page + + Background: + Given the following "categories" exist: + | name | category | idnumber | + | Category 1 | 0 | CAT1 | + | Category 2 | 0 | CAT2 | + | Category 3 | CAT2 | CAT3 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + | Course 2 | C2 | CAT1 | + | Course 3 | C3 | CAT2 | + | Course 4 | C4 | CAT3 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | First | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + | teacher1 | C3 | editingteacher | + + Scenario: Add the course list block on category page and navigate to the course listing + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I am on course index + And I follow "Miscellaneous" + And I add the "Courses" block + And I log out + When I log in as "teacher1" + And I am on course index + And I follow "Miscellaneous" + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: Add the course list block on category page and navigate to another course + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I am on course index + And I follow "Miscellaneous" + And I add the "Courses" block + And I log out + When I log in as "teacher1" + And I am on course index + And I follow "Miscellaneous" + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I am on "Course 3" course homepage + And I should see "Course 3" + + Scenario: Add the course list block on category page and view as an admin + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I am on course index + And I follow "Miscellaneous" + When I add the "Courses" block + Then I should see "Miscellaneous" in the "Course categories" "block" + And I should see "Category 1" in the "Course categories" "block" + And I should see "Category 2" in the "Course categories" "block" + And I should not see "Category 3" in the "Course categories" "block" + And I should not see "Course 1" in the "Course categories" "block" + And I should not see "Course 2" in the "Course categories" "block" + And I follow "All courses" + And I should see "Miscellaneous" diff --git a/course_list/tests/behat/block_course_list_course.feature b/course_list/tests/behat/block_course_list_course.feature new file mode 100644 index 0000000..8a14d9e --- /dev/null +++ b/course_list/tests/behat/block_course_list_course.feature @@ -0,0 +1,87 @@ +@block @block_course_list +Feature: Enable the course_list block on a course page and view it's contents + In order to enable the course list block on an course page + As a teacher + I can add the course list block to a course page + + Background: + Given the following "categories" exist: + | name | category | idnumber | + | Category 1 | 0 | CAT1 | + | Category 2 | 0 | CAT2 | + | Category 3 | CAT2 | CAT3 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + | Course 2 | C2 | CAT1 | + | Course 3 | C3 | CAT2 | + | Course 4 | C4 | CAT3 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | First | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + | teacher1 | C3 | editingteacher | + + Scenario: Add the course list block on course page and navigate to the course listing + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Courses" block + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: Add the course list block on course page and navigate to another course + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Courses" block + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I am on "Course 3" course homepage + And I should see "Course 3" + + Scenario: Add the course list block on course page and view as an admin + Given I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + When I add the "Courses" block + Then I should see "Miscellaneous" in the "Course categories" "block" + And I should see "Category 1" in the "Course categories" "block" + And I should see "Category 2" in the "Course categories" "block" + And I should not see "Category 3" in the "Course categories" "block" + And I should not see "Course 1" in the "Course categories" "block" + And I should not see "Course 2" in the "Course categories" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: View the course list block on course page with hide all courses link enabled + Given the following config values are set as admin: + | block_course_list_hideallcourseslink | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Courses" block + Then I should not see "All courses" in the "My courses" "block" + + Scenario: View the course list block on course page with admin sees own course enabled + Given the following config values are set as admin: + | block_course_list_adminview | own | + And the following "course enrolments" exist: + | user | course | role | + | admin | C1 | editingteacher | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + When I add the "Courses" block + Then I should not see "Miscellaneous" in the "My courses" "block" + And I should not see "Category 1" in the "My courses" "block" + And I should not see "Category 2" in the "My courses" "block" + And I should not see "Category 3" in the "My courses" "block" + And I should see "Course 1" in the "My courses" "block" + And I should not see "Course 2" in the "My courses" "block" + And I follow "All courses" + And I should see "Miscellaneous" diff --git a/course_list/tests/behat/block_course_list_dashboard.feature b/course_list/tests/behat/block_course_list_dashboard.feature new file mode 100644 index 0000000..f31ae94 --- /dev/null +++ b/course_list/tests/behat/block_course_list_dashboard.feature @@ -0,0 +1,61 @@ +@block @block_course_list +Feature: Enable the course_list block on the dashboard and view it's contents + In order to enable the course list block on the dashboard + As a user + I can add the course list block to the dashboard + + Background: + Given the following "categories" exist: + | name | category | idnumber | + | Category 1 | 0 | CAT1 | + | Category 2 | 0 | CAT2 | + | Category 3 | CAT2 | CAT3 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + | Course 2 | C2 | CAT1 | + | Course 3 | C3 | CAT2 | + | Course 4 | C4 | CAT3 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | First | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + | teacher1 | C3 | editingteacher | + + Scenario: Add the course list block on the dashboard and navigate to the course listing + Given I log in as "teacher1" + And I press "Customise this page" + When I add the "Courses" block + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: Add the course list block on the dashboard and navigate to another course + Given I log in as "teacher1" + And I press "Customise this page" + When I add the "Courses" block + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I am on "Course 3" course homepage + And I should see "Course 3" + + Scenario: Add the course list block on the dashboard and view as an admin + Given I log in as "admin" + And I press "Customise this page" + When I add the "Courses" block + Then I should see "Miscellaneous" in the "Course categories" "block" + And I should see "Category 1" in the "Course categories" "block" + And I should see "Category 2" in the "Course categories" "block" + And I should not see "Category 3" in the "Course categories" "block" + And I should not see "Course 1" in the "Course categories" "block" + And I should not see "Course 2" in the "Course categories" "block" + And I follow "All courses" + And I should see "Miscellaneous" diff --git a/course_list/tests/behat/block_course_list_frontpage.feature b/course_list/tests/behat/block_course_list_frontpage.feature new file mode 100644 index 0000000..b10cf27 --- /dev/null +++ b/course_list/tests/behat/block_course_list_frontpage.feature @@ -0,0 +1,86 @@ +@block @block_course_list +Feature: Enable the course_list block on the frontpage and view it's contents + In order to enable the course list block on the frontpage + As an admin + I can add the course list block to the frontpage + + Background: + Given the following "categories" exist: + | name | category | idnumber | + | Category 1 | 0 | CAT1 | + | Category 2 | 0 | CAT2 | + | Category 3 | CAT2 | CAT3 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + | Course 2 | C2 | CAT1 | + | Course 3 | C3 | CAT2 | + | Course 4 | C4 | CAT3 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | First | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + | teacher1 | C3 | editingteacher | + + Scenario: Add the course list block on the frontpage and navigate to the course listing + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Courses" block + And I log out + When I log in as "teacher1" + And I am on site homepage + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: Add the course list block on the frontpage page and navigate to another course + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Courses" block + And I log out + When I log in as "teacher1" + And I am on site homepage + Then I should see "Course 1" in the "My courses" "block" + And I should see "Course 2" in the "My courses" "block" + And I should see "Course 3" in the "My courses" "block" + And I should not see "Course 4" in the "My courses" "block" + And I am on "Course 3" course homepage + And I should see "Course 3" + + Scenario: Add the course list block on the frontpage page and view as an admin + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + When I add the "Courses" block + Then I should see "Miscellaneous" in the "Course categories" "block" + And I should see "Category 1" in the "Course categories" "block" + And I should see "Category 2" in the "Course categories" "block" + And I should not see "Category 3" in the "Course categories" "block" + And I should not see "Course 1" in the "Course categories" "block" + And I should not see "Course 2" in the "Course categories" "block" + And I follow "All courses" + And I should see "Miscellaneous" + + Scenario: Add the course list block on the frontpage page and view as a guest + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Courses" block + And I log out + When I log in as "guest" + Then I should see "Miscellaneous" in the "Course categories" "block" + And I should see "Category 1" in the "Course categories" "block" + And I should see "Category 2" in the "Course categories" "block" + And I should not see "Category 3" in the "Course categories" "block" + And I should not see "Course 1" in the "Course categories" "block" + And I should not see "Course 2" in the "Course categories" "block" + And I follow "All courses" + And I should see "Miscellaneous" diff --git a/course_list/version.php b/course_list/version.php new file mode 100644 index 0000000..35e5580 --- /dev/null +++ b/course_list/version.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/>. + +/** + * Version details + * + * @package block_course_list + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_course_list'; // Full name of the plugin (used for diagnostics) diff --git a/course_summary/block_course_summary.php b/course_summary/block_course_summary.php new file mode 100644 index 0000000..516c360 --- /dev/null +++ b/course_summary/block_course_summary.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/>. + +/** + * Course summary block + * + * @package block_course_summary + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_course_summary extends block_base { + + /** + * @var bool Flag to indicate whether the header should be hidden or not. + */ + private $headerhidden = true; + + function init() { + $this->title = get_string('pluginname', 'block_course_summary'); + } + + function applicable_formats() { + return array('all' => true, 'mod' => false, 'tag' => false, 'my' => false); + } + + function specialization() { + // Page type starts with 'course-view' and the page's course ID is not equal to the site ID. + if (strpos($this->page->pagetype, PAGE_COURSE_VIEW) === 0 && $this->page->course->id != SITEID) { + $this->title = get_string('coursesummary', 'block_course_summary'); + $this->headerhidden = false; + } + } + + function get_content() { + global $CFG, $OUTPUT; + + require_once($CFG->libdir . '/filelib.php'); + + if($this->content !== NULL) { + return $this->content; + } + + if (empty($this->instance)) { + return ''; + } + + $this->content = new stdClass(); + $options = new stdClass(); + $options->noclean = true; // Don't clean Javascripts etc + $options->overflowdiv = true; + $context = context_course::instance($this->page->course->id); + $this->page->course->summary = file_rewrite_pluginfile_urls($this->page->course->summary, 'pluginfile.php', $context->id, 'course', 'summary', NULL); + $this->content->text = format_text($this->page->course->summary, $this->page->course->summaryformat, $options); + $this->content->footer = ''; + + return $this->content; + } + + function hide_header() { + return $this->headerhidden; + } + +} + + diff --git a/course_summary/classes/privacy/provider.php b/course_summary/classes/privacy/provider.php new file mode 100644 index 0000000..4023e49 --- /dev/null +++ b/course_summary/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_course_summary. + * + * @package block_course_summary + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_course_summary\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_course_summary implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/course_summary/db/access.php b/course_summary/db/access.php new file mode 100644 index 0000000..a1e4265 --- /dev/null +++ b/course_summary/db/access.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/>. + +/** + * Course summary block caps. + * + * @package block_course_summary + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/course_summary: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/course_summary/db/upgrade.php b/course_summary/db/upgrade.php new file mode 100644 index 0000000..763a538 --- /dev/null +++ b/course_summary/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the course summary block + * + * 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. + * + * @since Moodle 2.0 + * @package block_course_summary + * @copyright 2012 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Handles upgrading instances of this block. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_course_summary_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/course_summary/lang/en/block_course_summary.php b/course_summary/lang/en/block_course_summary.php new file mode 100644 index 0000000..902dd5f --- /dev/null +++ b/course_summary/lang/en/block_course_summary.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/>. + +/** + * Strings for component 'block_course_summary', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_course_summary + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['coursesummary'] = 'Course summary'; +$string['course_summary:addinstance'] = 'Add a new course/site summary block'; +$string['pluginname'] = 'Course/site summary'; +$string['privacy:metadata'] = 'The Course/site summary block only shows information about courses and does not store data itself.'; diff --git a/course_summary/styles.css b/course_summary/styles.css new file mode 100644 index 0000000..5d37beb --- /dev/null +++ b/course_summary/styles.css @@ -0,0 +1,7 @@ +.block_course_summary .content { + padding: 10px; +} + +.block_course_summary .editbutton { + text-align: right; +} \ No newline at end of file diff --git a/course_summary/tests/behat/block_course_summary_course.feature b/course_summary/tests/behat/block_course_summary_course.feature new file mode 100644 index 0000000..3b4b32c --- /dev/null +++ b/course_summary/tests/behat/block_course_summary_course.feature @@ -0,0 +1,36 @@ +@block @block_course_summary +Feature: Course summary block used in a course + In order to help particpants know the summary of a course + As a teacher + I can add the course summary block to a course page + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | + | Course 1 | C101 | Proved the course summary block works! |0 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam | Student | student1@example.com | + | teacher1 | Teacher | One | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C101 | student | + | teacher1 | C101 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Course/site summary" block + And I log out + + Scenario: Student can view course summary + When I log in as "student1" + And I am on "Course 1" course homepage + Then "Course summary" "block" should exist + And I should see "Course summary" in the "Course summary" "block" + And I should see "Proved the course summary block works!" in the "Course summary" "block" + + Scenario: Teacher can not see edit icon when edit mode is off + When I log in as "teacher1" + And I am on "Course 1" course homepage + Then I should see "Proved the course summary block works!" in the "Course summary" "block" + And I should see "Course summary" in the "Course summary" "block" + And "Edit" "link" should not exist in the "Course summary" "block" diff --git a/course_summary/tests/behat/block_course_summary_frontpage.feature b/course_summary/tests/behat/block_course_summary_frontpage.feature new file mode 100644 index 0000000..a48fb70 --- /dev/null +++ b/course_summary/tests/behat/block_course_summary_frontpage.feature @@ -0,0 +1,30 @@ +@block @block_course_summary +Feature: Course summary block used on the frontpage + In order to help particpants know the summary of a site + As admin + I can use the course summary block on the frontpage + + Background: + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Course/site summary" block + And I navigate to "Edit settings" node in "Front page settings" + And I set the following fields to these values: + | summary | Proved the summary block works! | + And I press "Save changes" + And I log out + # The course summary block a default front page block, so no need to add it. + + Scenario: Guest can view site summary + When I am on site homepage + Then "Course/site summary" "block" should exist + And I should not see "Course summary" in the "Course/site summary" "block" + And I should see "Proved the summary block works!" in the "Course/site summary" "block" + + Scenario: Admin can not see edit icon when edit mode is off + When I log in as "admin" + And I am on site homepage + Then I should see "Proved the summary block works!" in the "Course/site summary" "block" + And I should not see "Course summary" in the "Course/site summary" "block" + And "Edit" "link" should not exist in the "Course/site summary" "block" diff --git a/course_summary/version.php b/course_summary/version.php new file mode 100644 index 0000000..62f27c5 --- /dev/null +++ b/course_summary/version.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/>. + +/** + * Version details + * + * @package block_course_summary + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_course_summary'; // Full name of the plugin (used for diagnostics) diff --git a/edit_form 14.04.12.php b/edit_form 14.04.12.php new file mode 100644 index 0000000..fac55d4 --- /dev/null +++ b/edit_form 14.04.12.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/>. + +/** + * Defines the base class form used by blocks/edit.php to edit block instance configuration. + * + * It works with the {@link block_edit_form} class, or rather the particular + * subclass defined by this block, to do the editing. + * + * @package core_block + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page +} + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/blocklib.php'); + +/** + * The base class form used by blocks/edit.php to edit block instance configuration. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_edit_form extends moodleform { + /** + * The block instance we are editing. + * @var block_base + */ + public $block; + /** + * The page we are editing this block in association with. + * @var moodle_page + */ + public $page; + + /** + * Defaults set in set_data() that need to be returned in get_data() if form elements were not created + * @var array + */ + protected $defaults = []; + + function __construct($actionurl, $block, $page) { + global $CFG; + $this->block = $block; + $this->page = $page; + parent::__construct($actionurl); + } + + function definition() { + $mform =& $this->_form; + + // First show fields specific to this type of block. + $this->specific_definition($mform); + + // Then show the fields about where this block appears. + $mform->addElement('header', 'whereheader', get_string('wherethisblockappears', 'block')); + + // If the current weight of the block is out-of-range, add that option in. + $blockweight = $this->block->instance->weight; + $weightoptions = array(); + if ($blockweight < -block_manager::MAX_WEIGHT) { + $weightoptions[$blockweight] = $blockweight; + } + for ($i = -block_manager::MAX_WEIGHT; $i <= block_manager::MAX_WEIGHT; $i++) { + $weightoptions[$i] = $i; + } + if ($blockweight > block_manager::MAX_WEIGHT) { + $weightoptions[$blockweight] = $blockweight; + } + $first = reset($weightoptions); + $weightoptions[$first] = get_string('bracketfirst', 'block', $first); + $last = end($weightoptions); + $weightoptions[$last] = get_string('bracketlast', 'block', $last); + + $regionoptions = $this->page->theme->get_all_block_regions(); + foreach ($this->page->blocks->get_regions() as $region) { + // Make sure to add all custom regions of this particular page too. + if (!isset($regionoptions[$region])) { + $regionoptions[$region] = $region; + } + } + + $parentcontext = context::instance_by_id($this->block->instance->parentcontextid); + $mform->addElement('static', 'bui_homecontext', get_string('createdat', 'block'), $parentcontext->get_context_name()); + $mform->addHelpButton('bui_homecontext', 'createdat', 'block'); + + // For pre-calculated (fixed) pagetype lists + $pagetypelist = array(); + + // parse pagetype patterns + $bits = explode('-', $this->page->pagetype); + + // First of all, check if we are editing blocks @ front-page or no and + // make some dark magic if so (MDL-30340) because each page context + // implies one (and only one) harcoded page-type that will be set later + // when processing the form data at {@link block_manager::process_url_edit()} + + // Front page, show the page-contexts element and set $pagetypelist to 'any page' (*) + // as unique option. Processign the form will do any change if needed + if ($this->is_editing_the_frontpage()) { + $contextoptions = array(); + $contextoptions[BUI_CONTEXTS_FRONTPAGE_ONLY] = get_string('showonfrontpageonly', 'block'); + $contextoptions[BUI_CONTEXTS_FRONTPAGE_SUBS] = get_string('showonfrontpageandsubs', 'block'); + $contextoptions[BUI_CONTEXTS_ENTIRE_SITE] = get_string('showonentiresite', 'block'); + $mform->addElement('select', 'bui_contexts', get_string('contexts', 'block'), $contextoptions); + $mform->addHelpButton('bui_contexts', 'contexts', 'block'); + $pagetypelist['*'] = '*'; // This is not going to be shown ever, it's an unique option + + // Any other system context block, hide the page-contexts element, + // it's always system-wide BUI_CONTEXTS_ENTIRE_SITE + } else if ($parentcontext->contextlevel == CONTEXT_SYSTEM) { + + } else if ($parentcontext->contextlevel == CONTEXT_COURSE) { + // 0 means display on current context only, not child contexts + // but if course managers select mod-* as pagetype patterns, block system will overwrite this option + // to 1 (display on current context and child contexts) + } else if ($parentcontext->contextlevel == CONTEXT_MODULE or $parentcontext->contextlevel == CONTEXT_USER) { + // module context doesn't have child contexts, so display in current context only + } else { + $parentcontextname = $parentcontext->get_context_name(); + $contextoptions[BUI_CONTEXTS_CURRENT] = get_string('showoncontextonly', 'block', $parentcontextname); + $contextoptions[BUI_CONTEXTS_CURRENT_SUBS] = get_string('showoncontextandsubs', 'block', $parentcontextname); + $mform->addElement('select', 'bui_contexts', get_string('contexts', 'block'), $contextoptions); + } + $mform->setType('bui_contexts', PARAM_INT); + + // Generate pagetype patterns by callbacks if necessary (has not been set specifically) + if (empty($pagetypelist)) { + $pagetypelist = generate_page_type_patterns($this->page->pagetype, $parentcontext, $this->page->context); + $displaypagetypewarning = false; + if (!array_key_exists($this->block->instance->pagetypepattern, $pagetypelist)) { + // Pushing block's existing page type pattern + $pagetypestringname = 'page-'.str_replace('*', 'x', $this->block->instance->pagetypepattern); + if (get_string_manager()->string_exists($pagetypestringname, 'pagetype')) { + $pagetypelist[$this->block->instance->pagetypepattern] = get_string($pagetypestringname, 'pagetype'); + } else { + //as a last resort we could put the page type pattern in the select box + //however this causes mod-data-view to be added if the only option available is mod-data-* + // so we are just showing a warning to users about their prev setting being reset + $displaypagetypewarning = true; + } + } + } + + // hide page type pattern select box if there is only one choice + if (count($pagetypelist) > 1) { + if ($displaypagetypewarning) { + $mform->addElement('static', 'pagetypewarning', '', get_string('pagetypewarning','block')); + } + + $mform->addElement('select', 'bui_pagetypepattern', get_string('restrictpagetypes', 'block'), $pagetypelist); + } else { + $values = array_keys($pagetypelist); + $value = array_pop($values); + // Now we are really hiding a lot (both page-contexts and page-type-patterns), + // specially in some systemcontext pages having only one option (my/user...) + // so, until it's decided if we are going to add the 'bring-back' pattern to + // all those pages or no (see MDL-30574), we are going to show the unique + // element statically + // TODO: Revisit this once MDL-30574 has been decided and implemented, although + // perhaps it's not bad to always show this statically when only one pattern is + // available. + if (!$this->is_editing_the_frontpage()) { + // Try to beautify it + $strvalue = $value; + $strkey = 'page-'.str_replace('*', 'x', $strvalue); + if (get_string_manager()->string_exists($strkey, 'pagetype')) { + $strvalue = get_string($strkey, 'pagetype'); + } + // Show as static (hidden has been set already) + $mform->addElement('static', 'bui_staticpagetypepattern', + get_string('restrictpagetypes','block'), $strvalue); + } + } + + if ($this->page->subpage) { + if ($parentcontext->contextlevel != CONTEXT_USER) { + $subpageoptions = array( + '%@NULL@%' => get_string('anypagematchingtheabove', 'block'), + $this->page->subpage => get_string('thisspecificpage', 'block', $this->page->subpage), + ); + $mform->addElement('select', 'bui_subpagepattern', get_string('subpages', 'block'), $subpageoptions); + } + } + + $defaultregionoptions = $regionoptions; + $defaultregion = $this->block->instance->defaultregion; + if (!array_key_exists($defaultregion, $defaultregionoptions)) { + $defaultregionoptions[$defaultregion] = $defaultregion; + } + $mform->addElement('select', 'bui_defaultregion', get_string('defaultregion', 'block'), $defaultregionoptions); + $mform->addHelpButton('bui_defaultregion', 'defaultregion', 'block'); + + $mform->addElement('select', 'bui_defaultweight', get_string('defaultweight', 'block'), $weightoptions); + $mform->addHelpButton('bui_defaultweight', 'defaultweight', 'block'); + + // Where this block is positioned on this page. + $mform->addElement('header', 'onthispage', get_string('onthispage', 'block')); + + $mform->addElement('selectyesno', 'bui_visible', get_string('visible', 'block')); + + $blockregion = $this->block->instance->region; + if (!array_key_exists($blockregion, $regionoptions)) { + $regionoptions[$blockregion] = $blockregion; + } + $mform->addElement('select', 'bui_region', get_string('region', 'block'), $regionoptions); + + $mform->addElement('select', 'bui_weight', get_string('weight', 'block'), $weightoptions); + + $pagefields = array('bui_visible', 'bui_region', 'bui_weight'); + if (!$this->block->user_can_edit()) { + $mform->hardFreezeAllVisibleExcept($pagefields); + } + if (!$this->page->user_can_edit_blocks()) { + $mform->hardFreeze($pagefields); + } + + $this->add_action_buttons(); + } + + /** + * Returns true if the user is editing a frontpage. + * @return bool + */ + public function is_editing_the_frontpage() { + // There are some conditions to check related to contexts. + $ctxconditions = $this->page->context->contextlevel == CONTEXT_COURSE && + $this->page->context->instanceid == get_site()->id; + $issiteindex = (strpos($this->page->pagetype, 'site-index') === 0); + // So now we can be 100% sure if edition is happening at frontpage. + return ($ctxconditions && $issiteindex); + } + + function set_data($defaults) { + // Prefix bui_ on all the core field names. + $blockfields = array('showinsubcontexts', 'pagetypepattern', 'subpagepattern', 'parentcontextid', + 'defaultregion', 'defaultweight', 'visible', 'region', 'weight'); + foreach ($blockfields as $field) { + $newname = 'bui_' . $field; + $defaults->$newname = $defaults->$field; + } + + // Copy block config into config_ fields. + if (!empty($this->block->config)) { + foreach ($this->block->config as $field => $value) { + $configfield = 'config_' . $field; + $defaults->$configfield = $value; + } + } + + // Munge ->subpagepattern becuase HTML selects don't play nicely with NULLs. + if (empty($defaults->bui_subpagepattern)) { + $defaults->bui_subpagepattern = '%@NULL@%'; + } + + $systemcontext = context_system::instance(); + if ($defaults->parentcontextid == $systemcontext->id) { + $defaults->bui_contexts = BUI_CONTEXTS_ENTIRE_SITE; // System-wide and sticky + } else { + $defaults->bui_contexts = $defaults->bui_showinsubcontexts; + } + + // Some fields may not be editable, remember the values here so we can return them in get_data(). + $this->defaults = [ + 'bui_parentcontextid' => $defaults->bui_parentcontextid, + 'bui_contexts' => $defaults->bui_contexts, + 'bui_pagetypepattern' => $defaults->bui_pagetypepattern, + 'bui_subpagepattern' => $defaults->bui_subpagepattern, + ]; + parent::set_data($defaults); + } + + /** + * Override this to create any form fields specific to this type of block. + * @param object $mform the form being built. + */ + protected function specific_definition($mform) { + // By default, do nothing. + } + + /** + * Return submitted data if properly submitted or returns NULL if validation fails or + * if there is no submitted data. + * + * @return object submitted data; NULL if not valid or not submitted or cancelled + */ + public function get_data() { + if ($data = parent::get_data()) { + // Blocklib expects 'bui_editingatfrontpage' property to be returned from this form. + $data->bui_editingatfrontpage = $this->is_editing_the_frontpage(); + // Some fields are non-editable and we need to populate them with the values from set_data(). + return (object)((array)$data + $this->defaults); + } + return $data; + } +} diff --git a/feedback/block_feedback.php b/feedback/block_feedback.php new file mode 100644 index 0000000..b10163a --- /dev/null +++ b/feedback/block_feedback.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/>. + +/** + * Feedback block. + * + * @package block_feedback + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/feedback/lib.php'); + +class block_feedback extends block_list { + + function init() { + $this->title = get_string('feedback', 'block_feedback'); + } + + function applicable_formats() { + return array('site' => true, 'course' => true); + } + + function get_content() { + global $CFG, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + $courseid = $this->page->course->id; + if ($courseid <= 0) { + $courseid = SITEID; + } + + $icon = $OUTPUT->image_icon('icon', get_string('pluginname', 'mod_feedback'), 'mod_feedback'); + + if (empty($this->instance->pageid)) { + $this->instance->pageid = SITEID; + } + + if ($feedbacks = feedback_get_feedbacks_from_sitecourse_map($courseid)) { + $baseurl = new moodle_url('/mod/feedback/view.php'); + foreach ($feedbacks as $feedback) { + $url = new moodle_url($baseurl); + $url->params(array('id'=>$feedback->cmid, 'courseid'=>$courseid)); + $this->content->items[] = '<a href="'.$url->out().'">'.$icon.$feedback->name.'</a>'; + } + } + + return $this->content; + } +} diff --git a/feedback/classes/privacy/provider.php b/feedback/classes/privacy/provider.php new file mode 100644 index 0000000..157e25e --- /dev/null +++ b/feedback/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_feedback. + * + * @package block_feedback + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_feedback\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_feedback implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/feedback/db/access.php b/feedback/db/access.php new file mode 100644 index 0000000..3dd660b --- /dev/null +++ b/feedback/db/access.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/>. + +/** + * Feedback block caps. + * + * @package block_feedback + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/feedback: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/feedback/db/install.php b/feedback/db/install.php new file mode 100644 index 0000000..737c9e3 --- /dev/null +++ b/feedback/db/install.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/>. + +/** + * Feedback block installation. + * + * @package block_feedback + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +function xmldb_block_feedback_install() { + global $DB; + +} + diff --git a/feedback/lang/en/block_feedback.php b/feedback/lang/en/block_feedback.php new file mode 100644 index 0000000..e9163f9 --- /dev/null +++ b/feedback/lang/en/block_feedback.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_feedback', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_feedback + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['feedback'] = 'Feedback'; +$string['feedback:addinstance'] = 'Add a new feedback block'; +$string['pluginname'] = 'Feedback'; +$string['privacy:metadata'] = 'The Feedback block only shows data stored in other locations.'; diff --git a/feedback/version.php b/feedback/version.php new file mode 100644 index 0000000..5043285 --- /dev/null +++ b/feedback/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 details + * + * @package block_feedback + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_feedback'; // Full name of the plugin (used for diagnostics) + +$plugin->dependencies = array('mod_feedback' => 2018050800); diff --git a/globalsearch/block_globalsearch.php b/globalsearch/block_globalsearch.php new file mode 100644 index 0000000..d3d96b2 --- /dev/null +++ b/globalsearch/block_globalsearch.php @@ -0,0 +1,96 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 search block. + * + * @package block_globalsearch + * @copyright Prateek Sachan {@link http://prateeksachan.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Global search block. + * + * @package block_globalsearch + * @copyright Prateek Sachan {@link http://prateeksachan.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_globalsearch extends block_base { + + /** + * Initialises the block. + * + * @return void + */ + public function init() { + $this->title = get_string('pluginname', 'block_globalsearch'); + } + + /** + * Gets the block contents. + * + * If we can avoid it better not check the server status here as connecting + * to the server will slow down the whole page load. + * + * @return string The block HTML. + */ + public function get_content() { + global $OUTPUT; + if ($this->content !== null) { + return $this->content; + } + + $this->content = new stdClass(); + $this->content->footer = ''; + + if (\core_search\manager::is_global_search_enabled() === false) { + $this->content->text = get_string('globalsearchdisabled', 'search'); + return $this->content; + } + + $url = new moodle_url('/search/index.php'); + $this->content->footer .= html_writer::link($url, get_string('advancedsearch', 'search')); + + $this->content->text = html_writer::start_tag('div', array('class' => 'searchform')); + $this->content->text .= html_writer::start_tag('form', array('action' => $url->out())); + $this->content->text .= html_writer::start_tag('fieldset', array('action' => 'invisiblefieldset')); + + // Input. + $this->content->text .= html_writer::tag('label', get_string('search', 'search'), + array('for' => 'searchform_search', 'class' => 'accesshide')); + $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'class' => 'form-control', + 'type' => 'text', 'size' => '15'); + $this->content->text .= html_writer::empty_tag('input', $inputoptions); + + // Context id. + if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) { + $this->content->text .= html_writer::empty_tag('input', ['type' => 'hidden', + 'name' => 'context', 'value' => $this->page->context->id]); + } + + // Search button. + $this->content->text .= html_writer::tag('button', get_string('search', 'search'), + array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch', 'class' => 'btn btn-secondary')); + $this->content->text .= html_writer::end_tag('fieldset'); + $this->content->text .= html_writer::end_tag('form'); + $this->content->text .= html_writer::end_tag('div'); + + return $this->content; + } +} diff --git a/globalsearch/classes/privacy/provider.php b/globalsearch/classes/privacy/provider.php new file mode 100644 index 0000000..0c57f73 --- /dev/null +++ b/globalsearch/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_globalsearch. + * + * @package block_globalsearch + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_globalsearch\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_globalsearch implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/globalsearch/db/access.php b/globalsearch/db/access.php new file mode 100644 index 0000000..ad9f49d --- /dev/null +++ b/globalsearch/db/access.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/>. + +/** + * Global search Block caps. + * + * @package block_globalsearch + * @copyright Prateek Sachan {@link http://prateeksachan.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/globalsearch:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/globalsearch:addinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), +); diff --git a/globalsearch/lang/en/block_globalsearch.php b/globalsearch/lang/en/block_globalsearch.php new file mode 100644 index 0000000..6fd2131 --- /dev/null +++ b/globalsearch/lang/en/block_globalsearch.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_globalsearch'. + * + * @package block_globalsearch + * @copyright Prateek Sachan {@link http://prateeksachan.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['globalsearch:addinstance'] = 'Add a new global search block'; +$string['globalsearch:myaddinstance'] = 'Add a new global search block to Dashboard'; +$string['pluginname'] = 'Global search'; +$string['privacy:metadata'] = 'The Global search block only shows data stored in other locations.'; diff --git a/globalsearch/styles.css b/globalsearch/styles.css new file mode 100644 index 0000000..6b94c4d --- /dev/null +++ b/globalsearch/styles.css @@ -0,0 +1,7 @@ +.block_globalsearch .searchform { + text-align: center; +} + +.block_globalsearch .footer { + text-align: center; +} diff --git a/globalsearch/version.php b/globalsearch/version.php new file mode 100644 index 0000000..332dce1 --- /dev/null +++ b/globalsearch/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 Search version details. + * + * @package block_globalsearch + * @copyright Prateek Sachan {@link http://prateeksachan.com} + * @copyright Daniel Neis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2018051400; +$plugin->requires = 2018050800; +$plugin->component = 'block_globalsearch'; diff --git a/glossary_random/backup/moodle2/restore_glossary_random_block_task.class.php b/glossary_random/backup/moodle2/restore_glossary_random_block_task.class.php new file mode 100644 index 0000000..236351c --- /dev/null +++ b/glossary_random/backup/moodle2/restore_glossary_random_block_task.class.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/>. + +/** + * @package block_glossary_random + * @subpackage backup-moodle2 + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Specialised restore task for the glossary_random block + * (using execute_after_tasks for recoding of glossaryid) + * + * TODO: Finish phpdocs + */ +class restore_glossary_random_block_task extends restore_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + } + + public function get_fileareas() { + return array(); // No associated fileareas + } + + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata + } + + /** + * This function, executed after all the tasks in the plan + * have been executed, will perform the recode of the + * target glossary for the block. This must be done here + * and not in normal execution steps because the glossary + * may be restored after the block. + */ + public function after_restore() { + global $DB; + + // Get the blockid + $blockid = $this->get_blockid(); + + // Extract block configdata and update it to point to the new glossary + if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) { + $config = unserialize(base64_decode($configdata)); + if (!empty($config->glossary)) { + if ($glossarymap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'glossary', $config->glossary)) { + // Get glossary mapping and replace it in config + $config->glossary = $glossarymap->newitemid; + } else if ($this->is_samesite()) { + // We are restoring on the same site, check if glossary can be used in the block in this course. + $glossaryid = $DB->get_field_sql("SELECT id FROM {glossary} " . + "WHERE id = ? AND (course = ? OR globalglossary = 1)", + [$config->glossary, $this->get_courseid()]); + if (!$glossaryid) { + unset($config->glossary); + } + } else { + // The block refers to a glossary not present in the backup file. + unset($config->glossary); + } + // Unset config variables that are no longer used. + unset($config->globalglossary); + unset($config->courseid); + // Save updated config. + $configdata = base64_encode(serialize($config)); + $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid)); + } + } + } + + static public function define_decode_contents() { + return array(); + } + + static public function define_decode_rules() { + return array(); + } +} diff --git a/glossary_random/block_glossary_random.php b/glossary_random/block_glossary_random.php new file mode 100644 index 0000000..5f15e8e --- /dev/null +++ b/glossary_random/block_glossary_random.php @@ -0,0 +1,258 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Glossary Random block. + * + * @package block_glossary_random + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('BGR_RANDOMLY', '0'); +define('BGR_LASTMODIFIED', '1'); +define('BGR_NEXTONE', '2'); +define('BGR_NEXTALPHA', '3'); + +class block_glossary_random extends block_base { + + /** + * @var cm_info|stdClass has properties 'id' (course module id) and 'uservisible' + * (whether the glossary is visible to the current user) + */ + protected $glossarycm = null; + + function init() { + $this->title = get_string('pluginname','block_glossary_random'); + } + + function specialization() { + global $CFG, $DB; + + require_once($CFG->libdir . '/filelib.php'); + + $this->course = $this->page->course; + + // load userdefined title and make sure it's never empty + if (empty($this->config->title)) { + $this->title = get_string('pluginname','block_glossary_random'); + } else { + $this->title = format_string($this->config->title, true, ['context' => $this->context]); + } + + if (empty($this->config->glossary)) { + return false; + } + + if (!isset($this->config->nexttime)) { + $this->config->nexttime = 0; + } + + //check if it's time to put a new entry in cache + if (time() > $this->config->nexttime) { + + if (!($cm = $this->get_glossary_cm()) || !$cm->uservisible) { + // Skip generating of the cache if we can't display anything to the current user. + return false; + } + + // place glossary concept and definition in $pref->cache + if (!$numberofentries = $DB->count_records('glossary_entries', + array('glossaryid'=>$this->config->glossary, 'approved'=>1))) { + $this->config->cache = get_string('noentriesyet','block_glossary_random'); + $this->instance_config_commit(); + } + + $glossaryctx = context_module::instance($cm->id); + + $limitfrom = 0; + $limitnum = 1; + + $orderby = 'timemodified ASC'; + + switch ($this->config->type) { + + case BGR_RANDOMLY: + $i = ($numberofentries > 1) ? rand(1, $numberofentries) : 1; + $limitfrom = $i-1; + break; + + case BGR_NEXTONE: + if (isset($this->config->previous)) { + $i = $this->config->previous + 1; + } else { + $i = 1; + } + if ($i > $numberofentries) { // Loop back to beginning + $i = 1; + } + $limitfrom = $i-1; + break; + + case BGR_NEXTALPHA: + $orderby = 'concept ASC'; + if (isset($this->config->previous)) { + $i = $this->config->previous + 1; + } else { + $i = 1; + } + if ($i > $numberofentries) { // Loop back to beginning + $i = 1; + } + $limitfrom = $i-1; + break; + + default: // BGR_LASTMODIFIED + $i = $numberofentries; + $limitfrom = 0; + $orderby = 'timemodified DESC, id DESC'; + break; + } + + if ($entry = $DB->get_records_sql("SELECT id, concept, definition, definitionformat, definitiontrust + FROM {glossary_entries} + WHERE glossaryid = ? AND approved = 1 + ORDER BY $orderby", array($this->config->glossary), $limitfrom, $limitnum)) { + + $entry = reset($entry); + + if (empty($this->config->showconcept)) { + $text = ''; + } else { + $text = "<h3>".format_string($entry->concept,true)."</h3>"; + } + + $options = new stdClass(); + $options->trusted = $entry->definitiontrust; + $options->overflowdiv = true; + $entry->definition = file_rewrite_pluginfile_urls($entry->definition, 'pluginfile.php', $glossaryctx->id, 'mod_glossary', 'entry', $entry->id); + $text .= format_text($entry->definition, $entry->definitionformat, $options); + + $this->config->nexttime = usergetmidnight(time()) + DAYSECS * $this->config->refresh; + $this->config->previous = $i; + + } else { + $text = get_string('noentriesyet','block_glossary_random'); + } + // store the text + $this->config->cache = $text; + $this->instance_config_commit(); + } + } + + /** + * Replace the instance's configuration data with those currently in $this->config; + */ + function instance_config_commit($nolongerused = false) { + // Unset config variables that are no longer used. + unset($this->config->globalglossary); + unset($this->config->courseid); + parent::instance_config_commit($nolongerused); + } + + /** + * Checks if glossary is available - it should be either located in the same course or be global + * + * @return null|cm_info|stdClass object with properties 'id' (course module id) and 'uservisible' + */ + protected function get_glossary_cm() { + global $DB; + if (empty($this->config->glossary)) { + // No glossary is configured. + return null; + } + + if (!empty($this->glossarycm)) { + return $this->glossarycm; + } + + if (!empty($this->page->course->id)) { + // First check if glossary belongs to the current course (we don't need to make any DB queries to find it). + $modinfo = get_fast_modinfo($this->page->course); + if (isset($modinfo->instances['glossary'][$this->config->glossary])) { + $this->glossarycm = $modinfo->instances['glossary'][$this->config->glossary]; + if ($this->glossarycm->uservisible) { + // The glossary is in the same course and is already visible to the current user, + // no need to check if it is global, save on DB query. + return $this->glossarycm; + } + } + } + + // Find course module id for the given glossary, only if it is global. + $cm = $DB->get_record_sql("SELECT cm.id, cm.visible AS uservisible + FROM {course_modules} cm + JOIN {modules} md ON md.id = cm.module + JOIN {glossary} g ON g.id = cm.instance + WHERE g.id = :instance AND md.name = :modulename AND g.globalglossary = 1", + ['instance' => $this->config->glossary, 'modulename' => 'glossary']); + + if ($cm) { + // This is a global glossary, create an object with properties 'id' and 'uservisible'. We don't need any + // other information so why bother retrieving it. Full access check is skipped for global glossaries for + // performance reasons. + $this->glossarycm = $cm; + } else if (empty($this->glossarycm)) { + // Glossary does not exist. Remove it in the config so we don't repeat this check again later. + $this->config->glossary = 0; + $this->instance_config_commit(); + } + + return $this->glossarycm; + } + + function instance_allow_multiple() { + // Are you going to allow multiple instances of each block? + // If yes, then it is assumed that the block WILL USE per-instance configuration + return true; + } + + function get_content() { + if ($this->content !== null) { + return $this->content; + } + $this->content = (object)['text' => '', 'footer' => '']; + + if (!$cm = $this->get_glossary_cm()) { + if ($this->user_can_edit()) { + $this->content->text = get_string('notyetconfigured', 'block_glossary_random'); + } + return $this->content; + } + + if (empty($this->config->cache)) { + $this->config->cache = ''; + } + + if ($cm->uservisible) { + // Show glossary if visible and place links in footer. + $this->content->text = $this->config->cache; + if (has_capability('mod/glossary:write', context_module::instance($cm->id))) { + $this->content->footer = html_writer::link(new moodle_url('/mod/glossary/edit.php', ['cmid' => $cm->id]), + format_string($this->config->addentry)) . '<br/>'; + } + + $this->content->footer .= html_writer::link(new moodle_url('/mod/glossary/view.php', ['id' => $cm->id]), + format_string($this->config->viewglossary)); + } else { + // Otherwise just place some text, no link. + $this->content->footer = format_string($this->config->invisible); + } + + return $this->content; + } +} + diff --git a/glossary_random/classes/privacy/provider.php b/glossary_random/classes/privacy/provider.php new file mode 100644 index 0000000..d1c64d4 --- /dev/null +++ b/glossary_random/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_glossary_random. + * + * @package block_glossary_random + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_glossary_random\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_glossary_random implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/glossary_random/db/access.php b/glossary_random/db/access.php new file mode 100644 index 0000000..0c1acd6 --- /dev/null +++ b/glossary_random/db/access.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/>. + +/** + * Glossary random block caps. + * + * @package block_glossary_random + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/glossary_random:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/glossary_random: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/glossary_random/edit_form.php b/glossary_random/edit_form.php new file mode 100644 index 0000000..ef1cf34 --- /dev/null +++ b/glossary_random/edit_form.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/>. + +/** + * Form for editing HTML block instances. + * + * @package block_glossary_random + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing Random glossary entry block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_glossary_random_edit_form extends block_edit_form { + protected function specific_definition($mform) { + global $DB; + + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('title', 'block_glossary_random')); + $mform->setDefault('config_title', get_string('pluginname','block_glossary_random')); + $mform->setType('config_title', PARAM_TEXT); + + // Select glossaries to put in dropdown box ... + $glossaries = $DB->get_records_select_menu('glossary', 'course = ? OR globalglossary = ?', array($this->block->course->id, 1), 'name', 'id,name'); + foreach($glossaries as $key => $value) { + $glossaries[$key] = strip_tags(format_string($value, true)); + } + $mform->addElement('select', 'config_glossary', get_string('select_glossary', 'block_glossary_random'), $glossaries); + + $mform->addElement('text', 'config_refresh', get_string('refresh', 'block_glossary_random'), array('size' => 5)); + $mform->setDefault('config_refresh', 0); + $mform->setType('config_refresh', PARAM_INT); + + // and select quotetypes to put in dropdown box + $types = array( + 0 => get_string('random','block_glossary_random'), + 1 => get_string('lastmodified','block_glossary_random'), + 2 => get_string('nextone','block_glossary_random'), + 3 => get_string('nextalpha','block_glossary_random') + ); + $mform->addElement('select', 'config_type', get_string('type', 'block_glossary_random'), $types); + + $mform->addElement('selectyesno', 'config_showconcept', get_string('showconcept', 'block_glossary_random')); + $mform->setDefault('config_showconcept', 1); + + $mform->addElement('static', 'footerdescription', '', get_string('whichfooter', 'block_glossary_random')); + + $mform->addElement('text', 'config_addentry', get_string('askaddentry', 'block_glossary_random')); + $mform->setDefault('config_addentry', get_string('addentry', 'block_glossary_random')); + $mform->setType('config_addentry', PARAM_NOTAGS); + + $mform->addElement('text', 'config_viewglossary', get_string('askviewglossary', 'block_glossary_random')); + $mform->setDefault('config_viewglossary', get_string('viewglossary', 'block_glossary_random')); + $mform->setType('config_viewglossary', PARAM_NOTAGS); + + $mform->addElement('text', 'config_invisible', get_string('askinvisible', 'block_glossary_random')); + $mform->setDefault('config_invisible', get_string('invisible', 'block_glossary_random')); + $mform->setType('config_invisible', PARAM_NOTAGS); + } +} diff --git a/glossary_random/lang/en/block_glossary_random.php b/glossary_random/lang/en/block_glossary_random.php new file mode 100644 index 0000000..1c58f6f --- /dev/null +++ b/glossary_random/lang/en/block_glossary_random.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/>. + +/** + * Strings for component 'block_glossary_random', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_glossary_random + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['addentry'] = 'Add a new entry'; +$string['askaddentry'] = 'When users can add entries to the glossary, show a link with this text'; +$string['askinvisible'] = 'When users cannot edit or view the glossary, show this text (without link)'; +$string['askviewglossary'] = 'When users can view the glossary but not add entries, show a link with this text'; +$string['glossary_random:addinstance'] = 'Add a new random glossary entry block'; +$string['glossary_random:myaddinstance'] = 'Add a new random glossary entry block to Dashboard'; +$string['intro'] = 'Make sure you have at least one glossary with at least one entry added to this course. Then you can adjust the following settings'; +$string['invisible'] = '(to be continued)'; +$string['lastmodified'] = 'Last modified entry'; +$string['nextalpha'] = 'Alphabetical order'; +$string['nextone'] = 'Next entry'; +$string['noentriesyet'] = 'There are no entries yet in the chosen glossary.'; +$string['notyetconfigured'] = 'Please configure this block using the edit icon.'; +$string['notyetglossary'] = 'You need to have at least one glossary to choose.'; +$string['pluginname'] = 'Random glossary entry'; +$string['random'] = 'Random entry'; +$string['refresh'] = 'Days before a new entry is chosen'; +$string['select_glossary'] = 'Take entries from this glossary'; +$string['showconcept'] = 'Show concept (heading) for each entry'; +$string['title'] = 'Title'; +$string['type'] = 'How a new entry is chosen'; +$string['viewglossary'] = 'View all entries'; +$string['whichfooter'] = 'You can display links to actions of the glossary this block is associated with. The block will only display links to actions which are enabled for that glossary.'; +$string['privacy:metadata'] = 'The Random glossary entry block only shows data stored in other locations.'; diff --git a/glossary_random/tests/behat/glossary_random.feature b/glossary_random/tests/behat/glossary_random.feature new file mode 100644 index 0000000..423c195 --- /dev/null +++ b/glossary_random/tests/behat/glossary_random.feature @@ -0,0 +1,108 @@ +@block @block_glossary_random +Feature: Random glossary entry block is used in a course + In order to show the entries from glossary + As a teacher + I can add the random glossary entry to a course page + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam1 | Student1 | student1@example.com | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + + Scenario: Student can not see the block if it is not configured + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Random glossary entry" block + Then I should see "Please configure this block using the edit icon" in the "block_glossary_random" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And "block_glossary_random" "block" should not exist + And I log out + + Scenario: View random (last) entry in the glossary with auto approval + Given the following "activities" exist: + | activity | name | intro | course | idnumber | defaultapproval | + | glossary | GlossaryAuto | Test glossary description | C1 | glossary1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Random glossary entry" block + And I configure the "block_glossary_random" block + And I set the following fields to these values: + | Title | AutoGlossaryblock | + | Take entries from this glossary | GlossaryAuto | + | How a new entry is chosen | Last modified entry | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "There are no entries yet in the chosen glossary" in the "AutoGlossaryblock" "block" + And I click on "Add a new entry" "link" in the "AutoGlossaryblock" "block" + And I set the following fields to these values: + | Concept | Concept1 | + | Definition | Definition1 | + And I press "Save changes" + And I am on "Course 1" course homepage + And I should see "Concept1" in the "AutoGlossaryblock" "block" + And I should see "Definition1" in the "AutoGlossaryblock" "block" + And I should not see "There are no entries yet in the chosen glossary" in the "AutoGlossaryblock" "block" + And I click on "Add a new entry" "link" in the "AutoGlossaryblock" "block" + And I set the following fields to these values: + | Concept | Concept2 | + | Definition | Definition2 | + And I press "Save changes" + And I am on "Course 1" course homepage + # Only the last entry appears in the block + And I should not see "Concept1" in the "AutoGlossaryblock" "block" + And I should not see "Definition1" in the "AutoGlossaryblock" "block" + And I should see "Concept2" in the "AutoGlossaryblock" "block" + And I should see "Definition2" in the "AutoGlossaryblock" "block" + And I click on "View all entries" "link" in the "AutoGlossaryblock" "block" + And I should see "GlossaryAuto" in the "#page-navbar" "css_element" + And I should see "Concept1" in the "#page-content" "css_element" + And I should see "Concept2" in the "#page-content" "css_element" + And I log out + + Scenario: View random (last) entry in the glossary with manual approval + Given the following "activities" exist: + | activity | name | intro | course | idnumber | defaultapproval | + | glossary | GlossaryManual | Test glossary description | C1 | glossary2 | 0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Random glossary entry" block + And I configure the "block_glossary_random" block + And I set the following fields to these values: + | Title | ManualGlossaryblock | + | Take entries from this glossary | GlossaryManual | + | How a new entry is chosen | Last modified entry | + And I press "Save changes" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" + And I click on "Add a new entry" "link" in the "ManualGlossaryblock" "block" + And I set the following fields to these values: + | Concept | Concept1 | + | Definition | Definition1 | + And I press "Save changes" + And I am on "Course 1" course homepage + And I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" + And I follow "GlossaryManual" + And I follow "Waiting approval" + And I follow "Approve" + And I click on "Course 1" "link" in the "#page-navbar" "css_element" + And I should see "Concept1" in the "ManualGlossaryblock" "block" + And I should see "Definition1" in the "ManualGlossaryblock" "block" + And I log out diff --git a/glossary_random/tests/behat/glossary_random_frontpage.feature b/glossary_random/tests/behat/glossary_random_frontpage.feature new file mode 100644 index 0000000..db31b91 --- /dev/null +++ b/glossary_random/tests/behat/glossary_random_frontpage.feature @@ -0,0 +1,27 @@ +@block @block_glossary_random +Feature: Random glossary entry block can be added to the frontpage + In order to show the entries from glossary on the frontpage + As a teacher + I can add the random glossary entry to the frontpage + + Scenario: Admin can add random glossary block to the frontpage + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | glossary | Tips and Tricks | Frontpage glossary description | Acceptance test site | glossary0 | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Random glossary entry" block + And I configure the "block_glossary_random" block + And I set the following fields to these values: + | Title | Tip of the day | + | Take entries from this glossary | Tips and Tricks | + And I press "Save changes" + And I click on "Add a new entry" "link" in the "Tip of the day" "block" + And I set the following fields to these values: + | Concept | Never come late | + | Definition | Come in time for your classes | + And I press "Save changes" + When I log out + Then I should see "Never come late" in the "Tip of the day" "block" + And I should see "Come in time for your classes" in the "Tip of the day" "block" diff --git a/glossary_random/tests/behat/glossary_random_global.feature b/glossary_random/tests/behat/glossary_random_global.feature new file mode 100644 index 0000000..3329702 --- /dev/null +++ b/glossary_random/tests/behat/glossary_random_global.feature @@ -0,0 +1,79 @@ +@block @block_glossary_random +Feature: Random glossary entry block linking to global glossary + In order to show the entries from glossary + As a teacher + I can add the random glossary entry to a course page + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + | Course 2 | C2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | globalglossary | defaultapproval | + | glossary | Tips and Tricks | Frontpage glossary description | C2 | glossary0 | 1 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam1 | Student1 | student1@example.com | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + + Scenario: View random (last) entry in the global glossary + When I log in as "admin" + And I am on "Course 2" course homepage + And I follow "Tips and Tricks" + And I press "Add a new entry" + And I set the following fields to these values: + | Concept | Never come late | + | Definition | Come in time for your classes | + And I press "Save changes" + And I log out + # As a teacher add a block to the course page linking to the global glossary. + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Random glossary entry" block + And I configure the "block_glossary_random" block + And I set the following fields to these values: + | Title | Tip of the day | + | Take entries from this glossary | Tips and Tricks | + | How a new entry is chosen | Last modified entry | + And I press "Save changes" + Then I should see "Never come late" in the "Tip of the day" "block" + And I should not see "Add a new entry" in the "Tip of the day" "block" + And I should see "View all entries" in the "Tip of the day" "block" + And I log out + # Student who can't see the module is still able to view entries in this block (because the glossary was marked as global) + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Never come late" in the "Tip of the day" "block" + And I should not see "Add a new entry" in the "Tip of the day" "block" + And I should see "View all entries" in the "Tip of the day" "block" + And I log out + + Scenario: Removing the global glossary that is used in random glossary block + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Random glossary entry" block + And I configure the "block_glossary_random" block + And I set the following fields to these values: + | Title | Tip of the day | + | Take entries from this glossary | Tips and Tricks | + | How a new entry is chosen | Last modified entry | + And I press "Save changes" + And I log out + And I log in as "admin" + And I am on "Course 2" course homepage + And I follow "Tips and Tricks" + And I follow "Edit settings" + And I set the field "globalglossary" to "0" + And I press "Save and return to course" + And I am on "Course 1" course homepage + Then I should see "Please configure this block using the edit icon." in the "Tip of the day" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And "Tip of the day" "block" should not exist + And I log out diff --git a/glossary_random/version.php b/glossary_random/version.php new file mode 100644 index 0000000..2a36e21 --- /dev/null +++ b/glossary_random/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 details + * + * @package block_glossary_random + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_glossary_random'; // Full name of the plugin (used for diagnostics) + +$plugin->dependencies = array('mod_glossary' => 2018050800); diff --git a/html/backup/moodle1/lib.php b/html/backup/moodle1/lib.php new file mode 100644 index 0000000..da215c2 --- /dev/null +++ b/html/backup/moodle1/lib.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/>. + +/** + * Provides support for the conversion of moodle1 backup to the moodle2 format + * + * @package block_html + * @copyright 2012 Paul Nicholls + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Block conversion handler for html + */ +class moodle1_block_html_handler extends moodle1_block_handler { + private $fileman = null; + protected function convert_configdata(array $olddata) { + global $CFG; + require_once($CFG->libdir . '/db/upgradelib.php'); + $instanceid = $olddata['id']; + $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']); + $decodeddata = base64_decode($olddata['configdata']); + list($updated, $configdata) = upgrade_fix_serialized_objects($decodeddata); + $configdata = unserialize($configdata); + + // get a fresh new file manager for this instance + $this->fileman = $this->converter->get_file_manager($contextid, 'block_html'); + + // convert course files embedded in the block content + $this->fileman->filearea = 'content'; + $this->fileman->itemid = 0; + $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman); + $configdata->format = FORMAT_HTML; + + return base64_encode(serialize($configdata)); + } + + protected function write_inforef_xml($newdata, $data) { + $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml"); + $this->xmlwriter->begin_tag('inforef'); + $this->xmlwriter->begin_tag('fileref'); + foreach ($this->fileman->get_fileids() as $fileid) { + $this->write_xml('file', array('id' => $fileid)); + } + $this->xmlwriter->end_tag('fileref'); + $this->xmlwriter->end_tag('inforef'); + $this->close_xml_writer(); + } +} diff --git a/html/backup/moodle2/backup_html_block_task.class.php b/html/backup/moodle2/backup_html_block_task.class.php new file mode 100644 index 0000000..7e1bccc --- /dev/null +++ b/html/backup/moodle2/backup_html_block_task.class.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/>. + +/** + * @package block_html + * @subpackage backup-moodle2 + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Specialised backup task for the html block + * (requires encode_content_links in some configdata attrs) + * + * TODO: Finish phpdocs + */ +class backup_html_block_task extends backup_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + } + + public function get_fileareas() { + return array('content'); + } + + public function get_configdata_encoded_attributes() { + return array('text'); // We need to encode some attrs in configdata + } + + static public function encode_content_links($content) { + return $content; // No special encoding of links + } +} + diff --git a/html/backup/moodle2/restore_html_block_task.class.php b/html/backup/moodle2/restore_html_block_task.class.php new file mode 100644 index 0000000..c3ce29b --- /dev/null +++ b/html/backup/moodle2/restore_html_block_task.class.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/>. + +/** + * @package block_html + * @subpackage backup-moodle2 + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Specialised restore task for the html block + * (requires encode_content_links in some configdata attrs) + * + * TODO: Finish phpdocs + */ +class restore_html_block_task extends restore_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + } + + public function get_fileareas() { + return array('content'); + } + + public function get_configdata_encoded_attributes() { + return array('text'); // We need to encode some attrs in configdata + } + + static public function define_decode_contents() { + + $contents = array(); + + $contents[] = new restore_html_block_decode_content('block_instances', 'configdata', 'block_instance'); + + return $contents; + } + + static public function define_decode_rules() { + return array(); + } +} + +/** + * Specialised restore_decode_content provider that unserializes the configdata + * field, to serve the configdata->text content to the restore_decode_processor + * packaging it back to its serialized form after process + */ +class restore_html_block_decode_content extends restore_decode_content { + + protected $configdata; // Temp storage for unserialized configdata + + protected function get_iterator() { + global $DB; + + // Build the SQL dynamically here + $fieldslist = 't.' . implode(', t.', $this->fields); + $sql = "SELECT t.id, $fieldslist + FROM {" . $this->tablename . "} t + JOIN {backup_ids_temp} b ON b.newitemid = t.id + WHERE b.backupid = ? + AND b.itemname = ? + AND t.blockname = 'html'"; + $params = array($this->restoreid, $this->mapping); + return ($DB->get_recordset_sql($sql, $params)); + } + + protected function preprocess_field($field) { + $this->configdata = unserialize(base64_decode($field)); + return isset($this->configdata->text) ? $this->configdata->text : ''; + } + + protected function postprocess_field($field) { + $this->configdata->text = $field; + return base64_encode(serialize($this->configdata)); + } +} diff --git a/html/block_html.php b/html/block_html.php new file mode 100644 index 0000000..505ff27 --- /dev/null +++ b/html/block_html.php @@ -0,0 +1,177 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 HTML block instances. + * + * @package block_html + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_html extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_html'); + } + + function has_config() { + return true; + } + + function applicable_formats() { + return array('all' => true); + } + + function specialization() { + if (isset($this->config->title)) { + $this->title = $this->title = format_string($this->config->title, true, ['context' => $this->context]); + } else { + $this->title = get_string('newhtmlblock', 'block_html'); + } + } + + function instance_allow_multiple() { + return true; + } + + function get_content() { + global $CFG; + + require_once($CFG->libdir . '/filelib.php'); + + if ($this->content !== NULL) { + return $this->content; + } + + $filteropt = new stdClass; + $filteropt->overflowdiv = true; + if ($this->content_is_trusted()) { + // fancy html allowed only on course, category and system blocks. + $filteropt->noclean = true; + } + + $this->content = new stdClass; + $this->content->footer = ''; + if (isset($this->config->text)) { + // rewrite url + $this->config->text = file_rewrite_pluginfile_urls($this->config->text, 'pluginfile.php', $this->context->id, 'block_html', 'content', NULL); + // Default to FORMAT_HTML which is what will have been used before the + // editor was properly implemented for the block. + $format = FORMAT_HTML; + // Check to see if the format has been properly set on the config + if (isset($this->config->format)) { + $format = $this->config->format; + } + $this->content->text = format_text($this->config->text, $format, $filteropt); + } else { + $this->content->text = ''; + } + + unset($filteropt); // memory footprint + + return $this->content; + } + + + /** + * Serialize and store config data + */ + function instance_config_save($data, $nolongerused = false) { + global $DB; + + $config = clone($data); + // Move embedded files into a proper filearea and adjust HTML links to match + $config->text = file_save_draft_area_files($data->text['itemid'], $this->context->id, 'block_html', 'content', 0, array('subdirs'=>true), $data->text['text']); + $config->format = $data->text['format']; + + parent::instance_config_save($config, $nolongerused); + } + + function instance_delete() { + global $DB; + $fs = get_file_storage(); + $fs->delete_area_files($this->context->id, 'block_html'); + return true; + } + + /** + * Copy any block-specific data when copying to a new block instance. + * @param int $fromid the id number of the block instance to copy from + * @return boolean + */ + public function instance_copy($fromid) { + $fromcontext = context_block::instance($fromid); + $fs = get_file_storage(); + // This extra check if file area is empty adds one query if it is not empty but saves several if it is. + if (!$fs->is_area_empty($fromcontext->id, 'block_html', 'content', 0, false)) { + $draftitemid = 0; + file_prepare_draft_area($draftitemid, $fromcontext->id, 'block_html', 'content', 0, array('subdirs' => true)); + file_save_draft_area_files($draftitemid, $this->context->id, 'block_html', 'content', 0, array('subdirs' => true)); + } + return true; + } + + function content_is_trusted() { + global $SCRIPT; + + if (!$context = context::instance_by_id($this->instance->parentcontextid, IGNORE_MISSING)) { + return false; + } + //find out if this block is on the profile page + if ($context->contextlevel == CONTEXT_USER) { + if ($SCRIPT === '/my/index.php') { + // this is exception - page is completely private, nobody else may see content there + // that is why we allow JS here + return true; + } else { + // no JS on public personal pages, it would be a big security issue + return false; + } + } + + return true; + } + + /** + * The block should only be dockable when the title of the block is not empty + * and when parent allows docking. + * + * @return bool + */ + public function instance_can_be_docked() { + return (!empty($this->config->title) && parent::instance_can_be_docked()); + } + + /* + * Add custom html attributes to aid with theming and styling + * + * @return array + */ + function html_attributes() { + global $CFG; + + $attributes = parent::html_attributes(); + + if (!empty($CFG->block_html_allowcssclasses)) { + if (!empty($this->config->classes)) { + $attributes['class'] .= ' '.$this->config->classes; + } + } + + return $attributes; + } +} diff --git a/html/classes/privacy/provider.php b/html/classes/privacy/provider.php new file mode 100644 index 0000000..21131fe --- /dev/null +++ b/html/classes/privacy/provider.php @@ -0,0 +1,196 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_html. + * + * @package block_html + * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_html\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\helper; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; + +/** + * Privacy Subsystem implementation for block_html. + * + * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The block_html block stores user provided data. + \core_privacy\local\metadata\provider, + + // The block_html block provides data directly to core. + \core_privacy\local\request\plugin\provider { + + /** + * Returns information about how block_html stores its data. + * + * @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->link_subsystem('block', 'privacy:metadata:block'); + + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { + // This block doesn't know who information is stored against unless it + // is at the user context. + $contextlist = new \core_privacy\local\request\contextlist(); + + $sql = "SELECT c.id + FROM {block_instances} b + INNER JOIN {context} c ON c.instanceid = b.id AND c.contextlevel = :contextblock + INNER JOIN {context} bpc ON bpc.id = b.parentcontextid + WHERE b.blockname = 'html' + AND bpc.contextlevel = :contextuser + AND bpc.instanceid = :userid"; + + $params = [ + 'contextblock' => CONTEXT_BLOCK, + 'contextuser' => CONTEXT_USER, + 'userid' => $userid, + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * 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; + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT + c.id AS contextid, + bi.* + FROM {context} c + INNER JOIN {block_instances} bi ON bi.id = c.instanceid AND c.contextlevel = :contextlevel + WHERE bi.blockname = 'html' + AND( + c.id {$contextsql} + ) + "; + + $params = [ + 'contextlevel' => CONTEXT_BLOCK, + ]; + $params += $contextparams; + + $instances = $DB->get_recordset_sql($sql, $params); + foreach ($instances as $instance) { + $context = \context_block::instance($instance->id); + $block = block_instance('html', $instance); + if (empty($block->config)) { + // Skip this block. It has not been configured. + continue; + } + + $html = writer::with_context($context) + ->rewrite_pluginfile_urls([], 'block_html', 'content', null, $block->config->text); + + // Default to FORMAT_HTML which is what will have been used before the + // editor was properly implemented for the block. + $format = isset($block->config->format) ? $block->config->format : FORMAT_HTML; + + $filteropt = (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + $html = format_text($html, $format, $filteropt); + + $data = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + $data->title = $block->config->title; + $data->content = $html; + + writer::with_context($context)->export_data([], $data); + } + $instances->close(); + } + + /** + * 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) { + + if (!$context instanceof \context_block) { + return; + } + + // The only way to delete data for the html block is to delete the block instance itself. + if ($blockinstance = static::get_instance_from_context($context)) { + blocks_delete_instance($blockinstance); + } + } + + /** + * 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) { + // The only way to delete data for the html block is to delete the block instance itself. + foreach ($contextlist as $context) { + + if (!$context instanceof \context_block) { + continue; + } + if ($blockinstance = static::get_instance_from_context($context)) { + blocks_delete_instance($blockinstance); + } + } + } + + /** + * Get the block instance record for the specified context. + * + * @param \context_block $context The context to fetch + * @return \stdClass + */ + protected static function get_instance_from_context(\context_block $context) { + global $DB; + + return $DB->get_record('block_instances', ['id' => $context->instanceid, 'blockname' => 'html']); + } +} diff --git a/html/classes/search/content.php b/html/classes/search/content.php new file mode 100644 index 0000000..32b20b9 --- /dev/null +++ b/html/classes/search/content.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/>. + +/** + * Search area for block_html blocks + * + * @package block_html + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_html\search; + +use core_search\moodle_recordset; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for block_html blocks + * + * @package block_html + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class content extends \core_search\base_block { + + public function get_document($record, $options = array()) { + // Create empty document. + $doc = \core_search\document_factory::instance($record->id, + $this->componentname, $this->areaname); + + // Get stdclass object with data from DB. + $data = unserialize(base64_decode($record->configdata)); + + // Get content. + $content = content_to_text($data->text, $data->format); + $doc->set('content', $content); + + if (isset($data->title)) { + // If there is a title, use it as title. + $doc->set('title', content_to_text($data->title, false)); + } else { + // If there is no title, use the content text again. + $doc->set('title', shorten_text($content)); + } + + // Set standard fields. + $doc->set('contextid', $record->contextid); + $doc->set('type', \core_search\manager::TYPE_TEXT); + $doc->set('courseid', $record->courseid); + $doc->set('modified', $record->timemodified); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); + + // Mark document new if appropriate. + if (isset($options['lastindexedtime']) && + ($options['lastindexedtime'] < $record->timecreated)) { + // If the document was created after the last index time, it must be new. + $doc->set_is_new(true); + } + + return $doc; + } + + public function uses_file_indexing() { + return true; + } + + public function attach_files($document) { + $fs = get_file_storage(); + + $context = \context::instance_by_id($document->get('contextid')); + + $files = $fs->get_area_files($context->id, 'block_html', 'content'); + foreach ($files as $file) { + $document->add_stored_file($file); + } + } +} diff --git a/html/db/access.php b/html/db/access.php new file mode 100644 index 0000000..84c5836 --- /dev/null +++ b/html/db/access.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/>. + +/** + * HTML block caps. + * + * @package block_html + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/html:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/html: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/html/db/upgrade.php b/html/db/upgrade.php new file mode 100644 index 0000000..5ad7f22 --- /dev/null +++ b/html/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the html block + * + * @since Moodle 2.0 + * @package block_html + * @copyright 2010 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade code for the HTML block. + * + * @param int $oldversion + */ +function xmldb_block_html_upgrade($oldversion) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/html/edit_form.php b/html/edit_form.php new file mode 100644 index 0000000..1f04ea7 --- /dev/null +++ b/html/edit_form.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/>. + +/** + * Form for editing HTML block instances. + * + * @package block_html + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing HTML block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_html_edit_form extends block_edit_form { + protected function specific_definition($mform) { + global $CFG; + + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitle', 'block_html')); + $mform->setType('config_title', PARAM_TEXT); + + $editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean'=>true, 'context'=>$this->block->context); + $mform->addElement('editor', 'config_text', get_string('configcontent', 'block_html'), null, $editoroptions); + $mform->addRule('config_text', null, 'required', null, 'client'); + $mform->setType('config_text', PARAM_RAW); // XSS is prevented when printing the block contents and serving files + + if (!empty($CFG->block_html_allowcssclasses)) { + $mform->addElement('text', 'config_classes', get_string('configclasses', 'block_html')); + $mform->setType('config_classes', PARAM_TEXT); + $mform->addHelpButton('config_classes', 'configclasses', 'block_html'); + } + } + + function set_data($defaults) { + if (!empty($this->block->config) && is_object($this->block->config)) { + $text = $this->block->config->text; + $draftid_editor = file_get_submitted_draft_itemid('config_text'); + if (empty($text)) { + $currenttext = ''; + } else { + $currenttext = $text; + } + $defaults->config_text['text'] = file_prepare_draft_area($draftid_editor, $this->block->context->id, 'block_html', 'content', 0, array('subdirs'=>true), $currenttext); + $defaults->config_text['itemid'] = $draftid_editor; + $defaults->config_text['format'] = $this->block->config->format; + } else { + $text = ''; + } + + if (!$this->block->user_can_edit() && !empty($this->block->config->title)) { + // If a title has been set but the user cannot edit it format it nicely + $title = $this->block->config->title; + $defaults->config_title = format_string($title, true, $this->page->context); + // Remove the title from the config so that parent::set_data doesn't set it. + unset($this->block->config->title); + } + + // have to delete text here, otherwise parent::set_data will empty content + // of editor + unset($this->block->config->text); + parent::set_data($defaults); + // restore $text + if (!isset($this->block->config)) { + $this->block->config = new stdClass(); + } + $this->block->config->text = $text; + if (isset($title)) { + // Reset the preserved title + $this->block->config->title = $title; + } + } +} diff --git a/html/lang/en/block_html.php b/html/lang/en/block_html.php new file mode 100644 index 0000000..6688816 --- /dev/null +++ b/html/lang/en/block_html.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/>. + +/** + * Strings for component 'block_html', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_html + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['allowadditionalcssclasses'] = 'Allow additional CSS classes'; +$string['configallowadditionalcssclasses'] = 'Adds a configuration option to HTML block instances allowing additional CSS classes to be set.'; +$string['configclasses'] = 'Additional CSS classes'; +$string['configclasses_help'] = 'The purpose of this configuration is to aid with theming by helping distinguish HTML blocks from each other. Any CSS classes entered here (space delimited) will be appended to the block\'s default classes.'; +$string['configcontent'] = 'Content'; +$string['configtitle'] = 'HTML block title'; +$string['html:addinstance'] = 'Add a new HTML block'; +$string['html:myaddinstance'] = 'Add a new HTML block to Dashboard'; +$string['newhtmlblock'] = '(new HTML block)'; +$string['pluginname'] = 'HTML'; +$string['search:content'] = 'HTML block content'; +$string['privacy:metadata:block'] = 'The HTML block stores all of its data within the block subsystem.'; diff --git a/html/lib.php b/html/lib.php new file mode 100644 index 0000000..a86b827 --- /dev/null +++ b/html/lib.php @@ -0,0 +1,112 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 HTML block instances. + * + * @copyright 2010 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package block_html + * @category files + * @param stdClass $course course object + * @param stdClass $birecord_or_cm block instance record + * @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 bool + * @todo MDL-36050 improve capability check on stick blocks, so we can check user capability before sending images. + */ +function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $args, $forcedownload, array $options=array()) { + global $DB, $CFG, $USER; + + if ($context->contextlevel != CONTEXT_BLOCK) { + send_file_not_found(); + } + + // If block is in course context, then check if user has capability to access course. + if ($context->get_course_context(false)) { + require_course_login($course); + } else if ($CFG->forcelogin) { + require_login(); + } else { + // Get parent context and see if user have proper permission. + $parentcontext = $context->get_parent_context(); + if ($parentcontext->contextlevel === CONTEXT_COURSECAT) { + // Check if category is visible and user can view this category. + $category = $DB->get_record('course_categories', array('id' => $parentcontext->instanceid), '*', MUST_EXIST); + if (!$category->visible) { + require_capability('moodle/category:viewhiddencategories', $parentcontext); + } + } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) { + // The block is in the context of a user, it is only visible to the user who it belongs to. + send_file_not_found(); + } + // At this point there is no way to check SYSTEM context, so ignoring it. + } + + if ($filearea !== 'content') { + send_file_not_found(); + } + + $fs = get_file_storage(); + + $filename = array_pop($args); + $filepath = $args ? '/'.implode('/', $args).'/' : '/'; + + if (!$file = $fs->get_file($context->id, 'block_html', 'content', 0, $filepath, $filename) or $file->is_directory()) { + send_file_not_found(); + } + + if ($parentcontext = context::instance_by_id($birecord_or_cm->parentcontextid, IGNORE_MISSING)) { + if ($parentcontext->contextlevel == CONTEXT_USER) { + // force download on all personal pages including /my/ + //because we do not have reliable way to find out from where this is used + $forcedownload = true; + } + } else { + // weird, there should be parent context, better force dowload then + $forcedownload = true; + } + + // NOTE: it woudl be nice to have file revisions here, for now rely on standard file lifetime, + // do not lower it because the files are dispalyed very often. + \core\session\manager::write_close(); + send_stored_file($file, null, 0, $forcedownload, $options); +} + +/** + * Perform global search replace such as when migrating site to new URL. + * @param $search + * @param $replace + * @return void + */ +function block_html_global_db_replace($search, $replace) { + global $DB; + + $instances = $DB->get_recordset('block_instances', array('blockname' => 'html')); + foreach ($instances as $instance) { + // TODO: intentionally hardcoded until MDL-26800 is fixed + $config = unserialize(base64_decode($instance->configdata)); + if (isset($config->text) and is_string($config->text)) { + $config->text = str_replace($search, $replace, $config->text); + $DB->update_record('block_instances', ['id' => $instance->id, + 'configdata' => base64_encode(serialize($config)), 'timemodified' => time()]); + } + } + $instances->close(); +} diff --git a/html/settings.php b/html/settings.php new file mode 100644 index 0000000..11c3a6e --- /dev/null +++ b/html/settings.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/>. + +/** + * Settings for the HTML block + * + * @copyright 2012 Aaron Barnes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package block_html + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configcheckbox('block_html_allowcssclasses', get_string('allowadditionalcssclasses', 'block_html'), + get_string('configallowadditionalcssclasses', 'block_html'), 0)); +} + + diff --git a/html/tests/behat/configuring_html_block.feature b/html/tests/behat/configuring_html_block.feature new file mode 100644 index 0000000..05a00d9 --- /dev/null +++ b/html/tests/behat/configuring_html_block.feature @@ -0,0 +1,41 @@ +@block @block_html @core_block +Feature: Adding and configuring HTML blocks + In order to have custom blocks on a page + As admin + I need to be able to create, configure and change HTML blocks + + @javascript + Scenario: Configuring the HTML block with Javascript on + Given I log in as "admin" + And I am on site homepage + When I turn editing mode on + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the field "Content" to "Static text without a header" + Then I should see "HTML block title" + And I press "Save changes" + Then I should not see "(new HTML block)" + And I configure the "block_html" block + And I set the field "HTML block title" to "The HTML block header" + And I set the field "Content" to "Static text with a header" + And I press "Save changes" + And "block_html" "block" should exist + And "The HTML block header" "block" should exist + And I should see "Static text with a header" in the "The HTML block header" "block" + + Scenario: Configuring the HTML block with Javascript off + Given I log in as "admin" + And I am on site homepage + When I turn editing mode on + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the field "Content" to "Static text without a header" + And I press "Save changes" + Then I should not see "(new HTML block)" + And I configure the "block_html" block + And I set the field "HTML block title" to "The HTML block header" + And I set the field "Content" to "Static text with a header" + And I press "Save changes" + And "block_html" "block" should exist + And "The HTML block header" "block" should exist + And I should see "Static text with a header" in the "The HTML block header" "block" diff --git a/html/tests/behat/course_block.feature b/html/tests/behat/course_block.feature new file mode 100644 index 0000000..3341177 --- /dev/null +++ b/html/tests/behat/course_block.feature @@ -0,0 +1,35 @@ +@block @block_html +Feature: HTML blocks in a course + In order to have one or multiple HTML blocks in a course + As a teacher + I need to be able to create and change such blocks + + Scenario: Adding HTML block in a course + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Terry1 | Teacher1 | teacher@example.com | + | student1 | Sam1 | Student1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the field "Content" to "First block content" + And I set the field "HTML block title" to "First block header" + And I press "Save changes" + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the field "Content" to "Second block content" + And I set the field "HTML block title" to "Second block header" + And I press "Save changes" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "First block content" in the "First block header" "block" + And I should see "Second block content" in the "Second block header" "block" diff --git a/html/tests/behat/multiple_instances.feature b/html/tests/behat/multiple_instances.feature new file mode 100644 index 0000000..3ed187b --- /dev/null +++ b/html/tests/behat/multiple_instances.feature @@ -0,0 +1,42 @@ +@block @block_html +Feature: Adding and configuring multiple HTML blocks + In order to have one or multiple HTML blocks on a page + As admin + I need to be able to create, configure and change HTML blocks + + Background: + Given I log in as "admin" + And I am on site homepage + When I turn editing mode on + And I add the "HTML" block + + Scenario: Other users can not see HTML block that has not been configured + Then "(new HTML block)" "block" should exist + And I log out + And "(new HTML block)" "block" should not exist + And "block_html" "block" should not exist + + Scenario: Other users can see HTML block that has been configured even when it has no header + And I configure the "(new HTML block)" block + And I set the field "Content" to "Static text without a header" + And I press "Save changes" + Then I should not see "(new HTML block)" + And I log out + And I am on homepage + And "block_html" "block" should exist + And I should see "Static text without a header" in the "block_html" "block" + And I should not see "(new HTML block)" + + Scenario: Adding multiple instances of HTML block on a page + And I configure the "block_html" block + And I set the field "HTML block title" to "The HTML block header" + And I set the field "Content" to "Static text with a header" + And I press "Save changes" + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the field "HTML block title" to "The second HTML block header" + And I set the field "Content" to "Second block contents" + And I press "Save changes" + And I log out + Then I should see "Static text with a header" in the "The HTML block header" "block" + And I should see "Second block contents" in the "The second HTML block header" "block" diff --git a/html/tests/privacy_provider_test.php b/html/tests/privacy_provider_test.php new file mode 100644 index 0000000..94d38ca --- /dev/null +++ b/html/tests/privacy_provider_test.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/>. + +/** + * Unit tests for the block_html implementation of the privacy API. + * + * @package block_html + * @category test + * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \block_html\privacy\provider; + +/** + * Unit tests for the block_html implementation of the privacy API. + * + * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_html_privacy_testcase extends \core_privacy\tests\provider_testcase { + /** + * Get the list of standard format options for comparison. + * + * @return \stdClass + */ + protected function get_format_options() { + return (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + } + + /** + * Creates an HTML block on a user. + * + * @param string $title + * @param string $body + * @param string $format + * @return \block_instance + */ + protected function create_user_block($title, $body, $format) { + global $USER; + + $configdata = (object) [ + 'title' => $title, + 'text' => [ + 'itemid' => 19, + 'text' => $body, + 'format' => $format, + ], + ]; + + $this->create_block($this->construct_user_page($USER)); + $block = $this->get_last_block_on_page($this->construct_user_page($USER)); + $block = block_instance('html', $block->instance); + $block->instance_config_save((object) $configdata); + + return $block; + } + + /** + * Creates an HTML block on a course. + * + * @param \stdClass $course + * @param string $title + * @param string $body + * @param string $format + * @return \block_instance + */ + protected function create_course_block($course, $title, $body, $format) { + global $USER; + + $configdata = (object) [ + 'title' => $title, + 'text' => [ + 'itemid' => 19, + 'text' => $body, + 'format' => $format, + ], + ]; + + $this->create_block($this->construct_course_page($course)); + $block = $this->get_last_block_on_page($this->construct_course_page($course)); + $block = block_instance('html', $block->instance); + $block->instance_config_save((object) $configdata); + + return $block; + } + + /** + * Creates an HTML block on a page. + * + * @param \page $page Page + */ + protected function create_block($page) { + $page->blocks->add_block_at_end_of_default_region('html'); + } + + /** + * Get the last block on the page. + * + * @param \page $page Page + * @return \block_html Block instance object + */ + protected function get_last_block_on_page($page) { + $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region()); + $block = end($blocks); + + return $block; + } + + /** + * Constructs a Page object for the User Dashboard. + * + * @param \stdClass $user User to create Dashboard for. + * @return \moodle_page + */ + protected function construct_user_page(\stdClass $user) { + $page = new \moodle_page(); + $page->set_context(\context_user::instance($user->id)); + $page->set_pagelayout('mydashboard'); + $page->set_pagetype('my-index'); + $page->blocks->load_blocks(); + return $page; + } + + /** + * Constructs a Page object for the User Dashboard. + * + * @param \stdClass $course Course to create Dashboard for. + * @return \moodle_page + */ + protected function construct_course_page(\stdClass $course) { + $page = new \moodle_page(); + $page->set_context(\context_course::instance($course->id)); + $page->set_pagelayout('standard'); + $page->set_pagetype('course-view'); + $page->set_course($course); + $page->blocks->load_blocks(); + return $page; + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_user_block() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // Only the user context should be returned. + $this->assertCount(1, $contextlist); + $this->assertEquals($context, $contextlist->current()); + + // Export the data. + $this->export_context_data_for_user($user->id, $context, 'block_html'); + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + // Check the data. + $data = $writer->get_data([]); + $this->assertInstanceOf('stdClass', $data); + $this->assertEquals($title, $data->title); + $this->assertEquals(format_text($content, $format, $this->get_format_options()), $data->content); + + // Delete the context. + provider::delete_data_for_all_users_in_context($context); + + // Re-fetch the contexts - it should no longer be returned. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard which is not configured is _not_ exported. + */ + public function test_user_block_unconfigured() { + global $DB; + + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $block = $this->create_user_block($title, $content, $format); + $block->instance->configdata = ''; + $DB->update_record('block_instances', $block->instance); + $block = block_instance('html', $block->instance); + + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // Only the user context should be returned. + $this->assertCount(1, $contextlist); + $this->assertEquals($context, $contextlist->current()); + + // Export the data. + $this->export_context_data_for_user($user->id, $context, 'block_html'); + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_user_multiple_blocks_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $blocks = []; + $contexts = []; + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // There are now two blocks on the user context. + $this->assertCount(2, $contextlist); + foreach ($contextlist as $context) { + $this->assertTrue(isset($contexts[$context->id])); + } + + // Turn them into an approved_contextlist. + $approvedlist = new approved_contextlist($user, 'block_html', $contextlist->get_contextids()); + + // Delete using delete_data_for_user. + provider::delete_data_for_user($approvedlist); + + // Re-fetch the contexts - it should no longer be returned. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard is not exported. + */ + public function test_course_blocks_not_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $this->setUser($user); + + $block = $this->create_course_block($course, $title, $content, $format); + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // No blocks should be returned. + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_mixed_multiple_blocks_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $contexts = []; + + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $this->setUser($user); + + $block = $this->create_course_block($course, $title, $content, $format); + $context = \context_block::instance($block->instance->id); + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // There are now two blocks on the user context. + $this->assertCount(2, $contextlist); + foreach ($contextlist as $context) { + $this->assertTrue(isset($contexts[$context->id])); + } + } +} diff --git a/html/tests/search_content_test.php b/html/tests/search_content_test.php new file mode 100644 index 0000000..5afb8cd --- /dev/null +++ b/html/tests/search_content_test.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/>. + +/** + * Unit test for search indexing. + * + * @package block_html + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_html; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit test for search indexing. + * + * @package block_html + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class search_content_testcase extends \advanced_testcase { + + /** + * Creates an HTML block on a course. + * + * @param \stdClass $course Course object + * @return \block_html Block instance object + */ + protected function create_block($course) { + $page = self::construct_page($course); + $page->blocks->add_block_at_end_of_default_region('html'); + + // Load the block. + $page = self::construct_page($course); + $page->blocks->load_blocks(); + $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region()); + $block = end($blocks); + return $block; + } + + /** + * Constructs a page object for the test course. + * + * @param \stdClass $course Moodle course object + * @return \moodle_page Page object representing course view + */ + protected static function construct_page($course) { + $context = \context_course::instance($course->id); + $page = new \moodle_page(); + $page->set_context($context); + $page->set_course($course); + $page->set_pagelayout('standard'); + $page->set_pagetype('course-view'); + $page->blocks->load_blocks(); + return $page; + } + + /** + * Tests all functionality in the search area. + */ + public function test_search_area() { + global $CFG, $USER, $DB; + require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create course and add HTML block. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $before = time(); + $block = $this->create_block($course); + + // Change block settings to add some text and a file. + $itemid = file_get_unused_draft_itemid(); + $fs = get_file_storage(); + $usercontext = \context_user::instance($USER->id); + $fs->create_file_from_string(['component' => 'user', 'filearea' => 'draft', + 'contextid' => $usercontext->id, 'itemid' => $itemid, 'filepath' => '/', + 'filename' => 'file.txt'], 'File content'); + $data = (object)['title' => 'Block title', 'text' => ['text' => 'Block text', + 'itemid' => $itemid, 'format' => FORMAT_HTML]]; + $block->instance_config_save($data); + $after = time(); + + // Set up fake search engine so we can create documents. + \testable_core_search::instance(); + + // Do indexing query. + $area = new \block_html\search\content(); + $this->assertEquals('html', $area->get_block_name()); + $rs = $area->get_recordset_by_timestamp(); + $count = 0; + foreach ($rs as $record) { + $count++; + + $this->assertEquals($course->id, $record->courseid); + + // Check context is correct. + $blockcontext = \context::instance_by_id($record->contextid); + $this->assertInstanceOf('\context_block', $blockcontext); + $coursecontext = $blockcontext->get_parent_context(); + $this->assertEquals($course->id, $coursecontext->instanceid); + + // Check created and modified times are correct. + $this->assertTrue($record->timecreated >= $before && $record->timecreated <= $after); + $this->assertTrue($record->timemodified >= $before && $record->timemodified <= $after); + + // Get config data. + $data = unserialize(base64_decode($record->configdata)); + $this->assertEquals('Block title', $data->title); + $this->assertEquals('Block text', $data->text); + $this->assertEquals(FORMAT_HTML, $data->format); + + // Check the get_document function 'new' flag. + $doc = $area->get_document($record, ['lastindexedtime' => 1]); + $this->assertTrue($doc->get_is_new()); + $doc = $area->get_document($record, ['lastindexedtime' => time() + 1]); + $this->assertFalse($doc->get_is_new()); + + // Check the attach_files function results in correct list of associated files. + $this->assertCount(0, $doc->get_files()); + $area->attach_files($doc); + $files = $doc->get_files(); + $this->assertCount(2, $files); + foreach ($files as $file) { + if ($file->is_directory()) { + continue; + } + $this->assertEquals('file.txt', $file->get_filename()); + $this->assertEquals('File content', $file->get_content()); + } + + // Check the document fields are all as expected. + $this->assertEquals('Block title', $doc->get('title')); + $this->assertEquals('Block text', $doc->get('content')); + $this->assertEquals($blockcontext->id, $doc->get('contextid')); + $this->assertEquals(\core_search\manager::TYPE_TEXT, $doc->get('type')); + $this->assertEquals($course->id, $doc->get('courseid')); + $this->assertEquals($record->timemodified, $doc->get('modified')); + $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid')); + + // Also check getting the doc url and context url. + $url = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $record->id); + $this->assertTrue($url->compare($area->get_doc_url($doc))); + $this->assertTrue($url->compare($area->get_context_url($doc))); + } + $rs->close(); + + // Should only be one HTML block systemwide. + $this->assertEquals(1, $count); + + // If we run the query starting from 1 second after now, there should be no results. + $rs = $area->get_recordset_by_timestamp($after + 1); + $count = 0; + foreach ($rs as $record) { + $count++; + } + $rs->close(); + $this->assertEquals(0, $count); + + // Create another block, but this time leave it empty (no data set). Hack the time though. + $block = $this->create_block($course); + $DB->set_field('block_instances', 'timemodified', + $after + 10, ['id' => $block->instance->id]); + $rs = $area->get_recordset_by_timestamp($after + 10); + $count = 0; + foreach ($rs as $record) { + // Because there is no configdata we don't index it. + $count++; + } + $rs->close(); + $this->assertEquals(0, $count); + } +} + diff --git a/html/version.php b/html/version.php new file mode 100644 index 0000000..07c49bd --- /dev/null +++ b/html/version.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/>. + +/** + * Version details + * + * @package block_html + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_html'; // Full name of the plugin (used for diagnostics) diff --git a/index.html b/index.html new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ + diff --git a/login/block_login.php b/login/block_login.php new file mode 100644 index 0000000..e17aea1 --- /dev/null +++ b/login/block_login.php @@ -0,0 +1,126 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block + * + * @package block_login + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_login extends block_base { + function init() { + $this->title = get_string('pluginname', 'block_login'); + } + + function applicable_formats() { + return array('site' => true); + } + + function get_content () { + global $USER, $CFG, $SESSION, $OUTPUT; + require_once($CFG->libdir . '/authlib.php'); + + $wwwroot = ''; + $signup = ''; + + if ($this->content !== NULL) { + return $this->content; + } + + $wwwroot = $CFG->wwwroot; + + if (signup_is_enabled()) { + $signup = $wwwroot . '/login/signup.php'; + } + // TODO: now that we have multiauth it is hard to find out if there is a way to change password + $forgot = $wwwroot . '/login/forgot_password.php'; + + + $username = get_moodle_cookie(); + + $this->content = new stdClass(); + $this->content->footer = ''; + $this->content->text = ''; + + if (!isloggedin() or isguestuser()) { // Show the block + if (empty($CFG->authloginviaemail)) { + $strusername = get_string('username'); + } else { + $strusername = get_string('usernameemail'); + } + + $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'">'; + + $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>'; + $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>'; + + $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>'; + + $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" /></div>'; + + if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) { + $checked = $username ? 'checked="checked"' : ''; + $this->content->text .= '<div class="form-check">'; + $this->content->text .= '<label class="form-check-label">'; + $this->content->text .= '<input type="checkbox" name="rememberusername" id="rememberusername" + class="form-check-input" value="1" '.$checked.'/> '; + $this->content->text .= get_string('rememberusername', 'admin').'</label>'; + $this->content->text .= '</div>'; + } + + $this->content->text .= '<div class="form-group">'; + $this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />'; + $this->content->text .= '</div>'; + + $this->content->text .= "</form>\n"; + + if (!empty($signup)) { + $this->content->text .= '<div><a href="'.$signup.'">'.get_string('startsignup').'</a></div>'; + } + if (!empty($forgot)) { + $this->content->text .= '<div><a href="'.$forgot.'">'.get_string('forgotaccount').'</a></div>'; + } + + $authsequence = get_enabled_auth_plugins(true); // Get all auths, in sequence. + $potentialidps = array(); + foreach ($authsequence as $authname) { + $authplugin = get_auth_plugin($authname); + $potentialidps = array_merge($potentialidps, $authplugin->loginpage_idp_list($this->page->url->out(false))); + } + + if (!empty($potentialidps)) { + $this->content->text .= '<div class="potentialidps">'; + $this->content->text .= '<h6>' . get_string('potentialidps', 'auth') . '</h6>'; + $this->content->text .= '<div class="potentialidplist">'; + foreach ($potentialidps as $idp) { + $this->content->text .= '<div class="potentialidp">'; + $this->content->text .= '<a class="btn btn-default btn-block" '; + $this->content->text .= 'href="' . $idp['url']->out() . '" title="' . s($idp['name']) . '">'; + if (!empty($idp['iconurl'])) { + $this->content->text .= '<img src="' . s($idp['iconurl']) . '" width="24" height="24" class="m-r-1"/>'; + } + $this->content->text .= s($idp['name']) . '</a></div>'; + } + $this->content->text .= '</div>'; + $this->content->text .= '</div>'; + } + } + + return $this->content; + } +} diff --git a/login/classes/privacy/provider.php b/login/classes/privacy/provider.php new file mode 100644 index 0000000..3978b4f --- /dev/null +++ b/login/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_login. + * + * @package block_login + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_login\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_login implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/login/db/access.php b/login/db/access.php new file mode 100644 index 0000000..51bc829 --- /dev/null +++ b/login/db/access.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/>. + +/** + * Login block caps. + * + * @package block_login + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/login: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/login/lang/en/block_login.php b/login/lang/en/block_login.php new file mode 100644 index 0000000..29b53ad --- /dev/null +++ b/login/lang/en/block_login.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/>. + +/** + * Strings for component 'block_login', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_login + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['login:addinstance'] = 'Add a new login block'; +$string['pluginname'] = 'Login'; +$string['privacy:metadata'] = 'The Login block only provides a way to log in and does not store any data itself.'; diff --git a/login/tests/behat/login_block.feature b/login/tests/behat/login_block.feature new file mode 100644 index 0000000..caaa06b --- /dev/null +++ b/login/tests/behat/login_block.feature @@ -0,0 +1,28 @@ +@block @block_login +Feature: Login from a block + In order to make it easier to login + As an user + In need to login through a block + + Background: + Given the following "users" exist: + | username | password | firstname | lastname | email | + | testuser | testpass | Test | User | student1@example.com | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Login" block + + Scenario: Login block visible to non-logged in users + Given I log out + When I am on homepage + Then "Login" "block" should exist + + Scenario: Login as student through login block + Given I log out + And I am on homepage + When I set the field "Username" to "testuser" + And I set the field "Password" to "testpass" + And I click on "Log in" "button" in the "Login" "block" + Then I should see "You are logged in as Test User" + And "Login" "block" should not exist diff --git a/login/version.php b/login/version.php new file mode 100644 index 0000000..b4caa4f --- /dev/null +++ b/login/version.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/>. + +/** + * Version details + * + * @package block_login + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_login'; // Full name of the plugin (used for diagnostics) diff --git a/lp/block_lp.php b/lp/block_lp.php new file mode 100644 index 0000000..53fede6 --- /dev/null +++ b/lp/block_lp.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/>. + +/** + * Block LP main file. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Block LP class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_lp extends block_base { + + /** + * Applicable formats. + * + * @return array + */ + public function applicable_formats() { + return array('site' => true, 'course' => true, 'my' => true); + } + + /** + * Init. + * + * @return void + */ + public function init() { + $this->title = get_string('pluginname', 'block_lp'); + } + + /** + * Get content. + * + * @return stdClass + */ + public function get_content() { + if (isset($this->content)) { + return $this->content; + } + $this->content = new stdClass(); + + if (!get_config('core_competency', 'enabled')) { + return $this->content; + } + + // Block needs a valid, non-guest user to be logged-in in order to display the user's learning plans. + if (isloggedin() && !isguestuser()) { + $summary = new \block_lp\output\summary(); + if (!$summary->has_content()) { + return $this->content; + } + + $renderer = $this->page->get_renderer('block_lp'); + $this->content->text = $renderer->render($summary); + $this->content->footer = ''; + } + + return $this->content; + } + +} diff --git a/lp/classes/output/competencies_to_review_page.php b/lp/classes/output/competencies_to_review_page.php new file mode 100644 index 0000000..ce15862 --- /dev/null +++ b/lp/classes/output/competencies_to_review_page.php @@ -0,0 +1,87 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Competencies to review renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\external\user_competency_exporter; +use core_user\external\user_summary_exporter; + +/** + * Competencies to review renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competencies_to_review_page implements renderable, templatable { + + /** @var array Competencies to review. */ + protected $compstoreview; + + /** + * Constructor. + */ + public function __construct() { + $this->compstoreview = api::list_user_competencies_to_review(0, 1000); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + $compstoreview = array(); + foreach ($this->compstoreview['competencies'] as $compdata) { + $ucexporter = new user_competency_exporter($compdata->usercompetency, + array('scale' => $compdata->competency->get_scale())); + $compexporter = new competency_exporter($compdata->competency, + array('context' => $compdata->competency->get_context())); + $userexporter = new user_summary_exporter($compdata->user); + $compstoreview[] = array( + 'usercompetency' => $ucexporter->export($output), + 'competency' => $compexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'competencies' => $compstoreview, + 'pluginbaseurl' => (new moodle_url('/blocks/lp'))->out(false), + ); + + return $data; + } + +} diff --git a/lp/classes/output/plans_to_review_page.php b/lp/classes/output/plans_to_review_page.php new file mode 100644 index 0000000..ddf0624 --- /dev/null +++ b/lp/classes/output/plans_to_review_page.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/>. + +/** + * Plans to review renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\external\plan_exporter; +use core_user\external\user_summary_exporter; + +/** + * Plans to review renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plans_to_review_page implements renderable, templatable { + + /** @var array Plans to review. */ + protected $planstoreview; + + /** + * Constructor. + */ + public function __construct() { + $this->planstoreview = api::list_plans_to_review(0, 1000); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + $planstoreview = array(); + foreach ($this->planstoreview['plans'] as $plandata) { + $planexporter = new plan_exporter($plandata->plan, array('template' => $plandata->template)); + $userexporter = new user_summary_exporter($plandata->owner); + $planstoreview[] = array( + 'plan' => $planexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'plans' => $planstoreview, + 'pluginbaseurl' => (new moodle_url('/blocks/lp'))->out(false), + ); + + return $data; + } + +} diff --git a/lp/classes/output/renderer.php b/lp/classes/output/renderer.php new file mode 100644 index 0000000..14e7783 --- /dev/null +++ b/lp/classes/output/renderer.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/>. + +/** + * Block LP renderer. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use renderable; + +/** + * Block LP renderer class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * @param renderable $page + * @return string + */ + public function render_competencies_to_review_page(renderable $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('block_lp/competencies_to_review_page', $data); + } + + /** + * Defer to template. + * @param renderable $page + * @return string + */ + public function render_plans_to_review_page(renderable $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('block_lp/plans_to_review_page', $data); + } + + /** + * Defer to template. + * @param renderable $summary + * @return string + */ + public function render_summary(renderable $summary) { + $data = $summary->export_for_template($this); + return parent::render_from_template('block_lp/summary', $data); + } + +} diff --git a/lp/classes/output/summary.php b/lp/classes/output/summary.php new file mode 100644 index 0000000..995f6d4 --- /dev/null +++ b/lp/classes/output/summary.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/>. + +/** + * Summary renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\external\plan_exporter; +use core_competency\external\user_competency_exporter; +use core_user\external\user_summary_exporter; +use core_competency\plan; +use core_competency\url; +use renderable; +use renderer_base; +use templatable; +use required_capability_exception; + +/** + * Summary renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class summary implements renderable, templatable { + + /** @var array Active plans. */ + protected $activeplans = array(); + /** @var array Competencies to review. */ + protected $compstoreview = array(); + /** @var array Plans to review. */ + protected $planstoreview = array(); + /** @var array Plans. */ + protected $plans = array(); + /** @var stdClass The user. */ + protected $user; + + /** + * Constructor. + * @param stdClass $user The user. + */ + public function __construct($user = null) { + global $USER; + if (!$user) { + $user = $USER; + } + $this->user = $user; + + // Get the plans. + try { + $this->plans = api::list_user_plans($this->user->id); + } catch (required_capability_exception $e) { + $this->plans = []; + } + + // Get the competencies to review. + $this->compstoreview = api::list_user_competencies_to_review(0, 3); + + // Get the plans to review. + $this->planstoreview = api::list_plans_to_review(0, 3); + } + + public function export_for_template(renderer_base $output) { + $plans = array(); + foreach ($this->plans as $plan) { + if (count($plans) >= 3) { + break; + } + if ($plan->get('status') == plan::STATUS_ACTIVE) { + $plans[] = $plan; + } + } + $activeplans = array(); + foreach ($plans as $plan) { + $planexporter = new plan_exporter($plan, array('template' => $plan->get_template())); + $activeplans[] = $planexporter->export($output); + } + + $compstoreview = array(); + foreach ($this->compstoreview['competencies'] as $compdata) { + $ucexporter = new user_competency_exporter($compdata->usercompetency, + array('scale' => $compdata->competency->get_scale())); + $compexporter = new competency_exporter($compdata->competency, + array('context' => $compdata->competency->get_context())); + $userexporter = new user_summary_exporter($compdata->user); + $compstoreview[] = array( + 'usercompetency' => $ucexporter->export($output), + 'competency' => $compexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $planstoreview = array(); + foreach ($this->planstoreview['plans'] as $plandata) { + $planexporter = new plan_exporter($plandata->plan, array('template' => $plandata->template)); + $userexporter = new user_summary_exporter($plandata->owner); + $planstoreview[] = array( + 'plan' => $planexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'hasplans' => !empty($this->plans), + 'hasactiveplans' => !empty($activeplans), + 'hasmoreplans' => count($this->plans) > count($activeplans), + 'activeplans' => $activeplans, + + 'compstoreview' => $compstoreview, + 'hascompstoreview' => $this->compstoreview['count'] > 0, + 'hasmorecompstoreview' => $this->compstoreview['count'] > 3, + + 'planstoreview' => $planstoreview, + 'hasplanstoreview' => $this->planstoreview['count'] > 0, + 'hasmoreplanstoreview' => $this->planstoreview['count'] > 3, + + 'plansurl' => url::plans($this->user->id)->out(false), + 'pluginbaseurl' => (new \moodle_url('/blocks/lp'))->out(false), + 'userid' => $this->user->id, + ); + + return $data; + } + + /** + * Returns whether there is content in the summary. + * + * @return boolean + */ + public function has_content() { + return !empty($this->plans) || $this->planstoreview['count'] > 0 || $this->compstoreview['count'] > 0; + } + +} diff --git a/lp/classes/privacy/provider.php b/lp/classes/privacy/provider.php new file mode 100644 index 0000000..602af6f --- /dev/null +++ b/lp/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_lp. + * + * @package block_lp + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_lp\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_lp implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/lp/competencies_to_review.php b/lp/competencies_to_review.php new file mode 100644 index 0000000..817620c --- /dev/null +++ b/lp/competencies_to_review.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/>. + +/** + * Competencies to review page. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} + +$toreviewstr = get_string('competenciestoreview', 'block_lp'); + +$url = new moodle_url('/blocks/lp/competencies_to_review.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_url($url); +$PAGE->set_title($toreviewstr); +$PAGE->set_pagelayout('standard'); +$PAGE->navbar->add($toreviewstr, $url); + +$output = $PAGE->get_renderer('block_lp'); +echo $output->header(); +echo $output->heading($toreviewstr); + +$page = new \block_lp\output\competencies_to_review_page(); +echo $output->render($page); + +echo $output->footer(); diff --git a/lp/db/access.php b/lp/db/access.php new file mode 100644 index 0000000..73da9c9 --- /dev/null +++ b/lp/db/access.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/>. + +/** + * Block LP capabilities. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + // Whether or not the user can add the block. + 'block/lp:addinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + + // Whether or not the user can add the block on their dashboard. + 'block/lp:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + + // Whether or not a user can see the block. + 'block/lp:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + +); diff --git a/lp/lang/en/block_lp.php b/lp/lang/en/block_lp.php new file mode 100644 index 0000000..520f710 --- /dev/null +++ b/lp/lang/en/block_lp.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 LP language strings. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['competenciestoreview'] = 'Competencies to review'; +$string['lp:addinstance'] = 'Add a new learning plans block'; +$string['lp:myaddinstance'] = 'Add a new learning plans block to Dashboard'; +$string['lp:view'] = 'View learning plans block'; +$string['myplans'] = 'My plans'; +$string['noactiveplans'] = 'No active plans at the moment.'; +$string['planstoreview'] = 'Plans to review'; +$string['pluginname'] = 'Learning plans'; +$string['viewmore'] = 'View more...'; +$string['viewotherplans'] = 'View other plans...'; +$string['privacy:metadata'] = 'The Learning plans block only shows data stored in other locations.'; diff --git a/lp/plans_to_review.php b/lp/plans_to_review.php new file mode 100644 index 0000000..8643bcf --- /dev/null +++ b/lp/plans_to_review.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/>. + +/** + * Competencies to review page. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} + +$toreviewstr = get_string('planstoreview', 'block_lp'); + +$url = new moodle_url('/blocks/lp/plans_to_review.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_url($url); +$PAGE->set_title($toreviewstr); +$PAGE->set_pagelayout('standard'); +$PAGE->navbar->add($toreviewstr, $url); + +$output = $PAGE->get_renderer('block_lp'); +echo $output->header(); +echo $output->heading($toreviewstr); + +$page = new \block_lp\output\plans_to_review_page(); +echo $output->render($page); + +echo $output->footer(); diff --git a/lp/styles.css b/lp/styles.css new file mode 100644 index 0000000..2f06516 --- /dev/null +++ b/lp/styles.css @@ -0,0 +1,17 @@ +.block_lp.block .content h3 { + padding: 0; + text-transform: none; +} + +.block_lp .sub-content { + padding: 0 15px; +} + +.block_lp ul { + list-style: none; + margin: 0; +} + +.block_lp ul .more { + padding-top: 10px; +} diff --git a/lp/templates/competencies_to_review_page.mustache b/lp/templates/competencies_to_review_page.mustache new file mode 100644 index 0000000..9038d47 --- /dev/null +++ b/lp/templates/competencies_to_review_page.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/>. +}} +{{! + Competencies to review. + + Classes required for JS: + * - + + Data attributes required for JS: + * - + + Context variables required for this template: + * competencies +}} + +<div data-region="competencies-to-review"> +<table class="generaltable fullwidth"> + <thead> + <tr> + <th scope="col">{{#str}}shortname, tool_lp{{/str}}</th> + <th scope="col">{{#str}}user{{/str}}</th> + <th scope="col">{{#str}}reviewstatus, tool_lp{{/str}}</th> + </tr> + </thead> + <tbody> + {{#competencies}} + <tr> + <td><a href="{{usercompetency.url}}">{{{competency.shortname}}}</a></td> + <td>{{user.fullname}}</td> + <td>{{usercompetency.statusname}}</td> + </tr> + {{/competencies}} + </tbody> +</table> +</div> diff --git a/lp/templates/plans_to_review_page.mustache b/lp/templates/plans_to_review_page.mustache new file mode 100644 index 0000000..124dcd3 --- /dev/null +++ b/lp/templates/plans_to_review_page.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/>. +}} +{{! + Plans to review. + + Classes required for JS: + * - + + Data attributes required for JS: + * - + + Context variables required for this template: + * plans +}} + +<div data-region="plans-to-review"> +<table class="generaltable fullwidth"> + <thead> + <tr> + <th scope="col">{{#str}}planname, tool_lp{{/str}}</th> + <th scope="col">{{#str}}user{{/str}}</th> + <th scope="col">{{#str}}reviewstatus, tool_lp{{/str}}</th> + </tr> + </thead> + <tbody> + {{#plans}} + <tr> + <td><a href="{{plan.url}}">{{{plan.name}}}</a></td> + <td>{{user.fullname}}</td> + <td>{{plan.statusname}}</td> + </tr> + {{/plans}} + </tbody> +</table> +</div> diff --git a/lp/templates/summary.mustache b/lp/templates/summary.mustache new file mode 100644 index 0000000..dc0943f --- /dev/null +++ b/lp/templates/summary.mustache @@ -0,0 +1,87 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + Summary. + + Classes required for JS: + * None + + Data attibutes required for JS: + * None + + Context variables required for this template: + * hasplans + * hasactiveplans + * activeplans + * hasmoreplans + * hascompstoreview + * compstoreview + * hasmorecompstoreview + * hasplanstoreview + * planstoreview + * hasmoreplanstoreview +}} +<div> + {{#hasplans}} + <h3>{{#str}}myplans, block_lp{{/str}}</h3> + <div class="sub-content"> + {{#hasactiveplans}} + <ul> + {{#activeplans}} + <li><a href="{{url}}">{{{name}}}</a></li> + {{/activeplans}} + {{#hasmoreplans}} + <li class="more"><a href="{{plansurl}}">{{#str}}viewmore, block_lp{{/str}}</a></li> + {{/hasmoreplans}} + </ul> + {{/hasactiveplans}} + {{^hasactiveplans}} + <p>{{#str}}noactiveplans, block_lp{{/str}} <a href="{{plansurl}}">{{#str}}viewotherplans, block_lp{{/str}}</a></p> + {{/hasactiveplans}} + </div> + {{/hasplans}} + {{#hascompstoreview}} + <h3>{{#str}}competenciestoreview, block_lp{{/str}}</h3> + <div class="sub-content"> + <ul> + {{#compstoreview}} + <li> + <a href="{{usercompetency.url}}">{{{competency.shortname}}}</a> ({{user.fullname}}) - {{usercompetency.statusname}} + </li> + {{/compstoreview}} + {{#hasmorecompstoreview}} + <li class="more"><a href="{{pluginbaseurl}}/competencies_to_review.php">{{#str}}viewmore, block_lp{{/str}}</a></li> + {{/hasmorecompstoreview}} + </ul> + </div> + {{/hascompstoreview}} + {{#hasplanstoreview}} + <h3>{{#str}}planstoreview, block_lp{{/str}}</h3> + <div class="sub-content"> + <ul> + {{#planstoreview}} + <li> + <a href="{{plan.url}}">{{{plan.name}}}</a> ({{user.fullname}}) - {{plan.statusname}} + </li> + {{/planstoreview}} + {{#hasmoreplanstoreview}} + <li class="more"><a href="{{pluginbaseurl}}/plans_to_review.php">{{#str}}viewmore, block_lp{{/str}}</a></li> + {{/hasmoreplanstoreview}} + </ul> + </div> + {{/hasplanstoreview}} +</div> diff --git a/lp/version.php b/lp/version.php new file mode 100644 index 0000000..8d99fe3 --- /dev/null +++ b/lp/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/>. + +/** + * Block LP version file. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; +$plugin->requires = 2018050800; +$plugin->component = 'block_lp'; +$plugin->dependencies = array( + 'tool_lp' => ANY_VERSION +); diff --git a/mahara_iena b/mahara_iena new file mode 160000 index 0000000..fa37238 --- /dev/null +++ b/mahara_iena @@ -0,0 +1 @@ +Subproject commit fa37238b9f08eeee40d97b3dbedb8666d6fa13c2 diff --git a/mentees/block_mentees.php b/mentees/block_mentees.php new file mode 100644 index 0000000..0c2ff91 --- /dev/null +++ b/mentees/block_mentees.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/>. + +/** + * Mentees block. + * + * @package block_mentees + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_mentees extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_mentees'); + } + + function applicable_formats() { + return array('all' => true, 'tag' => false); + } + + function specialization() { + $this->title = isset($this->config->title) ? $this->config->title : get_string('newmenteesblock', 'block_mentees'); + } + + function instance_allow_multiple() { + return true; + } + + function get_content() { + global $CFG, $USER, $DB; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass(); + + // get all the mentees, i.e. users you have a direct assignment to + $allusernames = get_all_user_name_fields(true, 'u'); + if ($usercontexts = $DB->get_records_sql("SELECT c.instanceid, c.instanceid, $allusernames + FROM {role_assignments} ra, {context} c, {user} u + WHERE ra.userid = ? + AND ra.contextid = c.id + AND c.instanceid = u.id + AND c.contextlevel = ".CONTEXT_USER, array($USER->id))) { + + $this->content->text = '<ul>'; + foreach ($usercontexts as $usercontext) { + $this->content->text .= '<li><a href="'.$CFG->wwwroot.'/user/view.php?id='.$usercontext->instanceid.'&course='.SITEID.'">'.fullname($usercontext).'</a></li>'; + } + $this->content->text .= '</ul>'; + } + + $this->content->footer = ''; + + return $this->content; + } + + /** + * Returns true if the block can be docked. + * The mentees block can only be docked if it has a non-empty title. + * @return bool + */ + public function instance_can_be_docked() { + return parent::instance_can_be_docked() && isset($this->config->title) && !empty($this->config->title); + } +} + diff --git a/mentees/classes/privacy/provider.php b/mentees/classes/privacy/provider.php new file mode 100644 index 0000000..ca86561 --- /dev/null +++ b/mentees/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_mentees. + * + * @package block_mentees + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_mentees\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_mentees implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/mentees/db/access.php b/mentees/db/access.php new file mode 100644 index 0000000..ba14b07 --- /dev/null +++ b/mentees/db/access.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/>. + +/** + * Mentees block caps. + * + * @package block_mentees + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/mentees:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/mentees: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/mentees/edit_form.php b/mentees/edit_form.php new file mode 100644 index 0000000..333497d --- /dev/null +++ b/mentees/edit_form.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/>. + +/** + * Form for editing Mentees block instances. + * + * @package block_mentees + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing Mentees block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_mentees_edit_form extends block_edit_form { + protected function specific_definition($mform) { + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitleblankhides', 'block_mentees')); + $mform->setType('config_title', PARAM_TEXT); + } +} \ No newline at end of file diff --git a/mentees/lang/en/block_mentees.php b/mentees/lang/en/block_mentees.php new file mode 100644 index 0000000..df160cd --- /dev/null +++ b/mentees/lang/en/block_mentees.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/>. + +/** + * Strings for component 'block_mentees', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_mentees + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['configtitle'] = 'Mentees block title'; +$string['configtitleblankhides'] = 'Mentees block title (no title if blank)'; +$string['mentees:addinstance'] = 'Add a new mentees block'; +$string['mentees:myaddinstance'] = 'Add a new mentees block to Dashboard'; +$string['newmenteesblock'] = '(new Mentees block)'; +$string['pluginname'] = 'Mentees'; +$string['privacy:metadata'] = 'The Mentees block only shows data stored in other locations.'; diff --git a/mentees/tests/behat/configuring_mentees_block.feature b/mentees/tests/behat/configuring_mentees_block.feature new file mode 100644 index 0000000..3b6df26 --- /dev/null +++ b/mentees/tests/behat/configuring_mentees_block.feature @@ -0,0 +1,18 @@ +@block @block_mentees @core_block +Feature: Adding and configuring Mentees blocks + In order to have a Mentees blocks on a page + As admin + I need to be able to insert and configure a Mentees blocks + + @javascript + Scenario: Configuring the Mentees block with Javascript on + Given I log in as "admin" + And I am on site homepage + When I turn editing mode on + And I add the "Mentees" block + And I configure the "(new Mentees block)" block + Then I should see "Mentees block title (no title if blank)" + And I set the field "Mentees block title (no title if blank)" to "The Mentees block header" + And I press "Save changes" + And "block_mentees" "block" should exist + Then "The Mentees block header" "block" should exist diff --git a/mentees/version.php b/mentees/version.php new file mode 100644 index 0000000..6c47867 --- /dev/null +++ b/mentees/version.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/>. + +/** + * Version details + * + * @package block_mentees + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_mentees'; // Full name of the plugin (used for diagnostics) diff --git a/mnet_hosts/block_mnet_hosts.php b/mnet_hosts/block_mnet_hosts.php new file mode 100644 index 0000000..9e0715c --- /dev/null +++ b/mnet_hosts/block_mnet_hosts.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/>. + +/** + * MNet hosts block. + * + * @package block_mnet_hosts + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_mnet_hosts extends block_list { + function init() { + $this->title = get_string('pluginname','block_mnet_hosts') ; + } + + function has_config() { + return false; + } + + function applicable_formats() { + if (has_capability('moodle/site:mnetlogintoremote', context_system::instance(), NULL, false)) { + return array('all' => true, 'mod' => false, 'tag' => false); + } else { + return array('site' => true); + } + } + + function get_content() { + global $CFG, $USER, $DB, $OUTPUT; + + // shortcut - only for logged in users! + if (!isloggedin() || isguestuser()) { + return false; + } + + if (\core\session\manager::is_loggedinas()) { + $this->content = new stdClass(); + $this->content->footer = html_writer::tag('span', + get_string('notpermittedtojumpas', 'mnet')); + return $this->content; + } + + // according to start_jump_session, + // remote users can't on-jump + // so don't show this block to them + if (is_mnet_remote_user($USER)) { + if (debugging() and !empty($CFG->debugdisplay)) { + $this->content = new stdClass(); + $this->content->footer = html_writer::tag('span', + get_string('error_localusersonly', 'block_mnet_hosts'), + array('class' => 'error')); + return $this->content; + } else { + return ''; + } + } + + if (!is_enabled_auth('mnet')) { + if (debugging() and !empty($CFG->debugdisplay)) { + $this->content = new stdClass(); + $this->content->footer = html_writer::tag('span', + get_string('error_authmnetneeded', 'block_mnet_hosts'), + array('class' => 'error')); + return $this->content; + } else { + return ''; + } + } + + if (!has_capability('moodle/site:mnetlogintoremote', context_system::instance(), NULL, false)) { + if (debugging() and !empty($CFG->debugdisplay)) { + $this->content = new stdClass(); + $this->content->footer = html_writer::tag('span', + get_string('error_roamcapabilityneeded', 'block_mnet_hosts'), + array('class' => 'error')); + return $this->content; + } else { + return ''; + } + } + + if ($this->content !== NULL) { + return $this->content; + } + + // TODO: Test this query - it's appropriate? It works? + // get the hosts and whether we are doing SSO with them + $sql = " + SELECT DISTINCT + h.id, + h.name, + h.wwwroot, + a.name as application, + a.display_name + FROM + {mnet_host} h, + {mnet_application} a, + {mnet_host2service} h2s_IDP, + {mnet_service} s_IDP, + {mnet_host2service} h2s_SP, + {mnet_service} s_SP + WHERE + h.id <> ? AND + h.id <> ? AND + h.id = h2s_IDP.hostid AND + h.deleted = 0 AND + h.applicationid = a.id AND + h2s_IDP.serviceid = s_IDP.id AND + s_IDP.name = 'sso_idp' AND + h2s_IDP.publish = '1' AND + h.id = h2s_SP.hostid AND + h2s_SP.serviceid = s_SP.id AND + s_SP.name = 'sso_idp' AND + h2s_SP.publish = '1' + ORDER BY + a.display_name, + h.name"; + + $hosts = $DB->get_records_sql($sql, array($CFG->mnet_localhost_id, $CFG->mnet_all_hosts_id)); + + $this->content = new stdClass(); + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + if ($hosts) { + foreach ($hosts as $host) { + if ($host->id == $USER->mnethostid) { + $url = new \moodle_url($host->wwwroot); + } else { + $url = new \moodle_url('/auth/mnet/jump.php', array('hostid' => $host->id)); + } + $this->content->items[] = html_writer::tag('a', + $OUTPUT->pix_icon("i/{$host->application}_host", get_string('server', 'block_mnet_hosts')) . s($host->name), + array('href' => $url->out(), 'title' => s($host->name)) + ); + } + } + + return $this->content; + } +} diff --git a/mnet_hosts/classes/privacy/provider.php b/mnet_hosts/classes/privacy/provider.php new file mode 100644 index 0000000..e70f864 --- /dev/null +++ b/mnet_hosts/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_mnet_hosts. + * + * @package block_mnet_hosts + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_mnet_hosts\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_mnet_hosts implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/mnet_hosts/db/access.php b/mnet_hosts/db/access.php new file mode 100644 index 0000000..43e2c32 --- /dev/null +++ b/mnet_hosts/db/access.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/>. + +/** + * Mnet hosts block caps. + * + * @package block_mnet_hosts + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/mnet_hosts:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/mnet_hosts: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/mnet_hosts/lang/en/block_mnet_hosts.php b/mnet_hosts/lang/en/block_mnet_hosts.php new file mode 100644 index 0000000..4275b75 --- /dev/null +++ b/mnet_hosts/lang/en/block_mnet_hosts.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/>. + +/** + * Strings for component 'block_mnet_hosts', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_mnet_hosts + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['error_authmnetneeded'] = 'MNet authentication plugin must be enabled to see the list of MNet network servers'; +$string['error_localusersonly'] = 'Remote users can not jump to other MNet network servers from this host'; +$string['error_roamcapabilityneeded'] = 'Users need the capability \'Roam to a remote application via MNet\' to see the list of MNet network servers'; +$string['mnet_hosts:addinstance'] = 'Add a new network servers block'; +$string['mnet_hosts:myaddinstance'] = 'Add a new network servers block to Dashboard'; +$string['pluginname'] = 'Network servers'; +$string['server'] = 'Server'; +$string['privacy:metadata'] = 'The Network servers block only allows interaction with Network servers and neither stores or exports data itself.'; diff --git a/mnet_hosts/version.php b/mnet_hosts/version.php new file mode 100644 index 0000000..87c8b62 --- /dev/null +++ b/mnet_hosts/version.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/>. + +/** + * Version details + * + * @package block_mnet_hosts + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_mnet_hosts'; // Full name of the plugin (used for diagnostics) diff --git a/moodleblock.class.php b/moodleblock.class.php new file mode 100644 index 0000000..a81f7ac --- /dev/null +++ b/moodleblock.class.php @@ -0,0 +1,778 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 parent class for moodle blocks, block_base. + * + * @package core_block + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +/// Constants + +/** + * Block type of list. Contents of block should be set as an associative array in the content object as items ($this->content->items). Optionally include footer text in $this->content->footer. + */ +define('BLOCK_TYPE_LIST', 1); + +/** + * Block type of text. Contents of block should be set to standard html text in the content object as items ($this->content->text). Optionally include footer text in $this->content->footer. + */ +define('BLOCK_TYPE_TEXT', 2); +/** + * Block type of tree. $this->content->items is a list of tree_item objects and $this->content->footer is a string. + */ +define('BLOCK_TYPE_TREE', 3); + +/** + * Class for describing a moodle block, all Moodle blocks derive from this class + * + * @author Jon Papaioannou + * @package core_block + */ +class block_base { + + /** + * Internal var for storing/caching translated strings + * @var string $str + */ + var $str; + + /** + * The title of the block to be displayed in the block title area. + * @var string $title + */ + var $title = NULL; + + /** + * The name of the block to be displayed in the block title area if the title is empty. + * @var string arialabel + */ + var $arialabel = NULL; + + /** + * The type of content that this block creates. Currently support options - BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT + * @var int $content_type + */ + var $content_type = BLOCK_TYPE_TEXT; + + /** + * An object to contain the information to be displayed in the block. + * @var stdObject $content + */ + var $content = NULL; + + /** + * The initialized instance of this block object. + * @var block $instance + */ + var $instance = NULL; + + /** + * The page that this block is appearing on. + * @var moodle_page + */ + public $page = NULL; + + /** + * This blocks's context. + * @var stdClass + */ + public $context = NULL; + + /** + * An object containing the instance configuration information for the current instance of this block. + * @var stdObject $config + */ + var $config = NULL; + + /** + * How often the cronjob should run, 0 if not at all. + * @var int $cron + */ + + var $cron = NULL; + +/// Class Functions + + /** + * Fake constructor to keep PHP5 happy + * + */ + function __construct() { + $this->init(); + } + + /** + * Function that can be overridden to do extra cleanup before + * the database tables are deleted. (Called once per block, not per instance!) + */ + function before_delete() { + } + + /** + * Returns the block name, as present in the class name, + * the database, the block directory, etc etc. + * + * @return string + */ + function name() { + // Returns the block name, as present in the class name, + // the database, the block directory, etc etc. + static $myname; + if ($myname === NULL) { + $myname = strtolower(get_class($this)); + $myname = substr($myname, strpos($myname, '_') + 1); + } + return $myname; + } + + /** + * Parent class version of this function simply returns NULL + * This should be implemented by the derived class to return + * the content object. + * + * @return stdObject + */ + function get_content() { + // This should be implemented by the derived class. + return NULL; + } + + /** + * Returns the class $title var value. + * + * Intentionally doesn't check if a title is set. + * This is already done in {@link _self_test()} + * + * @return string $this->title + */ + function get_title() { + // Intentionally doesn't check if a title is set. This is already done in _self_test() + return $this->title; + } + + /** + * Returns the class $content_type var value. + * + * Intentionally doesn't check if content_type is set. + * This is already done in {@link _self_test()} + * + * @return string $this->content_type + */ + function get_content_type() { + // Intentionally doesn't check if a content_type is set. This is already done in _self_test() + return $this->content_type; + } + + /** + * Returns true or false, depending on whether this block has any content to display + * and whether the user has permission to view the block + * + * @return boolean + */ + function is_empty() { + if ( !has_capability('moodle/block:view', $this->context) ) { + return true; + } + + $this->get_content(); + return(empty($this->content->text) && empty($this->content->footer)); + } + + /** + * First sets the current value of $this->content to NULL + * then calls the block's {@link get_content()} function + * to set its value back. + * + * @return stdObject + */ + function refresh_content() { + // Nothing special here, depends on content() + $this->content = NULL; + return $this->get_content(); + } + + /** + * Return a block_contents object representing the full contents of this block. + * + * This internally calls ->get_content(), and then adds the editing controls etc. + * + * You probably should not override this method, but instead override + * {@link html_attributes()}, {@link formatted_contents()} or {@link get_content()}, + * {@link hide_header()}, {@link (get_edit_controls)}, etc. + * + * @return block_contents a representation of the block, for rendering. + * @since Moodle 2.0. + */ + public function get_content_for_output($output) { + global $CFG; + + $bc = new block_contents($this->html_attributes()); + $bc->attributes['data-block'] = $this->name(); + $bc->blockinstanceid = $this->instance->id; + $bc->blockpositionid = $this->instance->blockpositionid; + + if ($this->instance->visible) { + $bc->content = $this->formatted_contents($output); + if (!empty($this->content->footer)) { + $bc->footer = $this->content->footer; + } + } else { + $bc->add_class('invisible'); + } + + if (!$this->hide_header()) { + $bc->title = $this->title; + } + + if (empty($bc->title)) { + $bc->arialabel = new lang_string('pluginname', get_class($this)); + $this->arialabel = $bc->arialabel; + } + + if ($this->page->user_is_editing()) { + $bc->controls = $this->page->blocks->edit_controls($this); + } else { + // we must not use is_empty on hidden blocks + if ($this->is_empty() && !$bc->controls) { + return null; + } + } + + if (empty($CFG->allowuserblockhiding) + || (empty($bc->content) && empty($bc->footer)) + || !$this->instance_can_be_collapsed()) { + $bc->collapsible = block_contents::NOT_HIDEABLE; + } else if (get_user_preferences('block' . $bc->blockinstanceid . 'hidden', false)) { + $bc->collapsible = block_contents::HIDDEN; + } else { + $bc->collapsible = block_contents::VISIBLE; + } + + if ($this->instance_can_be_docked() && !$this->hide_header()) { + $bc->dockable = true; + } + + $bc->annotation = ''; // TODO MDL-19398 need to work out what to say here. + + return $bc; + } + + /** + * Convert the contents of the block to HTML. + * + * This is used by block base classes like block_list to convert the structured + * $this->content->list and $this->content->icons arrays to HTML. So, in most + * blocks, you probaby want to override the {@link get_contents()} method, + * which generates that structured representation of the contents. + * + * @param $output The core_renderer to use when generating the output. + * @return string the HTML that should appearn in the body of the block. + * @since Moodle 2.0. + */ + protected function formatted_contents($output) { + $this->get_content(); + $this->get_required_javascript(); + if (!empty($this->content->text)) { + return $this->content->text; + } else { + return ''; + } + } + + /** + * Tests if this block has been implemented correctly. + * Also, $errors isn't used right now + * + * @return boolean + */ + + function _self_test() { + // Tests if this block has been implemented correctly. + // Also, $errors isn't used right now + $errors = array(); + + $correct = true; + if ($this->get_title() === NULL) { + $errors[] = 'title_not_set'; + $correct = false; + } + if (!in_array($this->get_content_type(), array(BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT, BLOCK_TYPE_TREE))) { + $errors[] = 'invalid_content_type'; + $correct = false; + } + //following selftest was not working when roles&capabilities were used from block +/* if ($this->get_content() === NULL) { + $errors[] = 'content_not_set'; + $correct = false; + }*/ + $formats = $this->applicable_formats(); + if (empty($formats) || array_sum($formats) === 0) { + $errors[] = 'no_formats'; + $correct = false; + } + + return $correct; + } + + /** + * Subclasses should override this and return true if the + * subclass block has a settings.php file. + * + * @return boolean + */ + function has_config() { + return false; + } + + /** + * Default behavior: save all variables as $CFG properties + * You don't need to override this if you 're satisfied with the above + * + * @deprecated since Moodle 2.9 MDL-49385 - Please use Admin Settings functionality to save block configuration. + */ + function config_save($data) { + throw new coding_exception('config_save() can not be used any more, use Admin Settings functionality to save block configuration.'); + } + + /** + * Which page types this block may appear on. + * + * The information returned here is processed by the + * {@link blocks_name_allowed_in_format()} function. Look there if you need + * to know exactly how this works. + * + * Default case: everything except mod and tag. + * + * @return array page-type prefix => true/false. + */ + function applicable_formats() { + // Default case: the block can be used in courses and site index, but not in activities + return array('all' => true, 'mod' => false, 'tag' => false); + } + + + /** + * Default return is false - header will be shown + * @return boolean + */ + function hide_header() { + return false; + } + + /** + * Return any HTML attributes that you want added to the outer <div> that + * of the block when it is output. + * + * Because of the way certain JS events are wired it is a good idea to ensure + * that the default values here still get set. + * I found the easiest way to do this and still set anything you want is to + * override it within your block in the following way + * + * <code php> + * function html_attributes() { + * $attributes = parent::html_attributes(); + * $attributes['class'] .= ' mynewclass'; + * return $attributes; + * } + * </code> + * + * @return array attribute name => value. + */ + function html_attributes() { + $attributes = array( + 'id' => 'inst' . $this->instance->id, + 'class' => 'block_' . $this->name(). ' block', + 'role' => $this->get_aria_role() + ); + if ($this->hide_header()) { + $attributes['class'] .= ' no-header'; + } + if ($this->instance_can_be_docked() && get_user_preferences('docked_block_instance_'.$this->instance->id, 0)) { + $attributes['class'] .= ' dock_on_load'; + } + return $attributes; + } + + /** + * Set up a particular instance of this class given data from the block_insances + * table and the current page. (See {@link block_manager::load_blocks()}.) + * + * @param stdClass $instance data from block_insances, block_positions, etc. + * @param moodle_page $the page this block is on. + */ + function _load_instance($instance, $page) { + if (!empty($instance->configdata)) { + $this->config = unserialize(base64_decode($instance->configdata)); + } + $this->instance = $instance; + $this->context = context_block::instance($instance->id); + $this->page = $page; + $this->specialization(); + } + + /** + * Allows the block to load any JS it requires into the page. + * + * By default this function simply permits the user to dock the block if it is dockable. + */ + function get_required_javascript() { + if ($this->instance_can_be_docked() && !$this->hide_header()) { + user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT); + } + } + + /** + * This function is called on your subclass right after an instance is loaded + * Use this function to act on instance data just after it's loaded and before anything else is done + * For instance: if your block will have different title's depending on location (site, course, blog, etc) + */ + function specialization() { + // Just to make sure that this method exists. + } + + /** + * Is each block of this type going to have instance-specific configuration? + * Normally, this setting is controlled by {@link instance_allow_multiple()}: if multiple + * instances are allowed, then each will surely need its own configuration. However, in some + * cases it may be necessary to provide instance configuration to blocks that do not want to + * allow multiple instances. In that case, make this function return true. + * I stress again that this makes a difference ONLY if {@link instance_allow_multiple()} returns false. + * @return boolean + */ + function instance_allow_config() { + return false; + } + + /** + * Are you going to allow multiple instances of each block? + * If yes, then it is assumed that the block WILL USE per-instance configuration + * @return boolean + */ + function instance_allow_multiple() { + // Are you going to allow multiple instances of each block? + // If yes, then it is assumed that the block WILL USE per-instance configuration + return false; + } + + /** + * Serialize and store config data + */ + function instance_config_save($data, $nolongerused = false) { + global $DB; + $DB->update_record('block_instances', ['id' => $this->instance->id, + 'configdata' => base64_encode(serialize($data)), 'timemodified' => time()]); + } + + /** + * Replace the instance's configuration data with those currently in $this->config; + */ + function instance_config_commit($nolongerused = false) { + global $DB; + $this->instance_config_save($this->config); + } + + /** + * Do any additional initialization you may need at the time a new block instance is created + * @return boolean + */ + function instance_create() { + return true; + } + + /** + * Copy any block-specific data when copying to a new block instance. + * @param int $fromid the id number of the block instance to copy from + * @return boolean + */ + public function instance_copy($fromid) { + return true; + } + + /** + * Delete everything related to this instance if you have been using persistent storage other than the configdata field. + * @return boolean + */ + function instance_delete() { + return true; + } + + /** + * Allows the block class to have a say in the user's ability to edit (i.e., configure) blocks of this type. + * The framework has first say in whether this will be allowed (e.g., no editing allowed unless in edit mode) + * but if the framework does allow it, the block can still decide to refuse. + * @return boolean + */ + function user_can_edit() { + global $USER; + + if (has_capability('moodle/block:edit', $this->context)) { + return true; + } + + // The blocks in My Moodle are a special case. We want them to inherit from the user context. + if (!empty($USER->id) + && $this->instance->parentcontextid == $this->page->context->id // Block belongs to this page + && $this->page->context->contextlevel == CONTEXT_USER // Page belongs to a user + && $this->page->context->instanceid == $USER->id) { // Page belongs to this user + return has_capability('moodle/my:manageblocks', $this->page->context); + } + + return false; + } + + /** + * Allows the block class to have a say in the user's ability to create new instances of this block. + * The framework has first say in whether this will be allowed (e.g., no adding allowed unless in edit mode) + * but if the framework does allow it, the block can still decide to refuse. + * This function has access to the complete page object, the creation related to which is being determined. + * + * @param moodle_page $page + * @return boolean + */ + function user_can_addto($page) { + global $USER; + + // The blocks in My Moodle are a special case and use a different capability. + if (!empty($USER->id) + && $page->context->contextlevel == CONTEXT_USER // Page belongs to a user + && $page->context->instanceid == $USER->id // Page belongs to this user + && $page->pagetype == 'my-index') { // Ensure we are on the My Moodle page + + // If the block cannot be displayed on /my it is ok if the myaddinstance capability is not defined. + $formats = $this->applicable_formats(); + // Is 'my' explicitly forbidden? + // If 'all' has not been allowed, has 'my' been explicitly allowed? + if ((isset($formats['my']) && $formats['my'] == false) + || (empty($formats['all']) && empty($formats['my']))) { + + // Block cannot be added to /my regardless of capabilities. + return false; + } else { + $capability = 'block/' . $this->name() . ':myaddinstance'; + return $this->has_add_block_capability($page, $capability) + && has_capability('moodle/my:manageblocks', $page->context); + } + } + + $capability = 'block/' . $this->name() . ':addinstance'; + if ($this->has_add_block_capability($page, $capability) + && has_capability('moodle/block:edit', $page->context)) { + return true; + } + + return false; + } + + /** + * Returns true if the user can add a block to a page. + * + * @param moodle_page $page + * @param string $capability the capability to check + * @return boolean true if user can add a block, false otherwise. + */ + private function has_add_block_capability($page, $capability) { + // Check if the capability exists. + if (!get_capability_info($capability)) { + // Debug warning that the capability does not exist, but no more than once per page. + static $warned = array(); + if (!isset($warned[$this->name()])) { + debugging('The block ' .$this->name() . ' does not define the standard capability ' . + $capability , DEBUG_DEVELOPER); + $warned[$this->name()] = 1; + } + // If the capability does not exist, the block can always be added. + return true; + } else { + return has_capability($capability, $page->context); + } + } + + static function get_extra_capabilities() { + return array('moodle/block:view', 'moodle/block:edit'); + } + + /** + * Can be overridden by the block to prevent the block from being dockable. + * + * @return bool + */ + public function instance_can_be_docked() { + global $CFG; + return (!empty($CFG->allowblockstodock) && $this->page->theme->enable_dock); + } + + /** + * If overridden and set to false by the block it will not be hidable when + * editing is turned on. + * + * @return bool + */ + public function instance_can_be_hidden() { + return true; + } + + /** + * If overridden and set to false by the block it will not be collapsible. + * + * @return bool + */ + public function instance_can_be_collapsed() { + return true; + } + + /** @callback callback functions for comments api */ + public static function comment_template($options) { + $ret = <<<EOD +<div class="comment-userpicture">___picture___</div> +<div class="comment-content"> + ___name___ - <span>___time___</span> + <div>___content___</div> +</div> +EOD; + return $ret; + } + public static function comment_permissions($options) { + return array('view'=>true, 'post'=>true); + } + public static function comment_url($options) { + return null; + } + public static function comment_display($comments, $options) { + return $comments; + } + public static function comment_add(&$comments, $options) { + return true; + } + + /** + * Returns the aria role attribute that best describes this block. + * + * Region is the default, but this should be overridden by a block is there is a region child, or even better + * a landmark child. + * + * Options are as follows: + * - landmark + * - application + * - banner + * - complementary + * - contentinfo + * - form + * - main + * - navigation + * - search + * + * @return string + */ + public function get_aria_role() { + return 'complementary'; + } +} + +/** + * Specialized class for displaying a block with a list of icons/text labels + * + * The get_content method should set $this->content->items and (optionally) + * $this->content->icons, instead of $this->content->text. + * + * @author Jon Papaioannou + * @package core_block + */ + +class block_list extends block_base { + var $content_type = BLOCK_TYPE_LIST; + + function is_empty() { + if ( !has_capability('moodle/block:view', $this->context) ) { + return true; + } + + $this->get_content(); + return (empty($this->content->items) && empty($this->content->footer)); + } + + protected function formatted_contents($output) { + $this->get_content(); + $this->get_required_javascript(); + if (!empty($this->content->items)) { + return $output->list_block_contents($this->content->icons, $this->content->items); + } else { + return ''; + } + } + + function html_attributes() { + $attributes = parent::html_attributes(); + $attributes['class'] .= ' list_block'; + return $attributes; + } + +} + +/** + * Specialized class for displaying a tree menu. + * + * The {@link get_content()} method involves setting the content of + * <code>$this->content->items</code> with an array of {@link tree_item} + * objects (these are the top-level nodes). The {@link tree_item::children} + * property may contain more tree_item objects, and so on. The tree_item class + * itself is abstract and not intended for use, use one of it's subclasses. + * + * Unlike {@link block_list}, the icons are specified as part of the items, + * not in a separate array. + * + * @author Alan Trick + * @package core_block + * @internal this extends block_list so we get is_empty() for free + */ +class block_tree extends block_list { + + /** + * @var int specifies the manner in which contents should be added to this + * block type. In this case <code>$this->content->items</code> is used with + * {@link tree_item}s. + */ + public $content_type = BLOCK_TYPE_TREE; + + /** + * Make the formatted HTML ouput. + * + * Also adds the required javascript call to the page output. + * + * @param core_renderer $output + * @return string HTML + */ + protected function formatted_contents($output) { + // based of code in admin_tree + global $PAGE; // TODO change this when there is a proper way for blocks to get stuff into head. + static $eventattached; + if ($eventattached===null) { + $eventattached = true; + } + if (!$this->content) { + $this->content = new stdClass; + $this->content->items = array(); + } + $this->get_required_javascript(); + $this->get_content(); + $content = $output->tree_block_contents($this->content->items,array('class'=>'block_tree list')); + if (isset($this->id) && !is_numeric($this->id)) { + $content = $output->box($content, 'block_tree_box', $this->id); + } + return $content; + } +} diff --git a/myoverview/amd/build/calendar_events_repository.min.js b/myoverview/amd/build/calendar_events_repository.min.js new file mode 100644 index 0000000..5c6e35a --- /dev/null +++ b/myoverview/amd/build/calendar_events_repository.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=20,e=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_course",args:a},f=b.call([e])[0];return f.fail(c.exception),f},f=function(a){a.hasOwnProperty("limit")||(a.limit=10),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var d={methodname:"core_calendar_get_action_events_by_courses",args:a},e=b.call([d])[0];return e.fail(c.exception),e},g=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_timesort",args:a},f=b.call([e])[0];return f.fail(c.exception),f};return{queryByTime:g,queryByCourse:e,queryByCourses:f}}); \ No newline at end of file diff --git a/myoverview/amd/build/event_list.min.js b/myoverview/amd/build/event_list.min.js new file mode 100644 index 0000000..0d04b59 --- /dev/null +++ b/myoverview/amd/build/event_list.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/templates","core/custom_interaction_events","block_myoverview/calendar_events_repository"],function(a,b,c,d,e){var f=86400,g={EMPTY_MESSAGE:'[data-region="empty-message"]',ROOT:'[data-region="event-list-container"]',EVENT_LIST:'[data-region="event-list"]',EVENT_LIST_CONTENT:'[data-region="event-list-content"]',EVENT_LIST_GROUP_CONTAINER:'[data-region="event-list-group-container"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',VIEW_MORE_BUTTON:'[data-action="view-more"]'},h={EVENT_LIST_ITEMS:"block_myoverview/event-list-items",COURSE_EVENT_LIST_ITEMS:"block_myoverview/course-event-list-items"},i=function(a){a.attr("data-loaded-all",!0)},j=function(a){return!!a.attr("data-loaded-all")},k=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.addClass("loading"),b.removeClass("hidden"),c.prop("disabled",!0)},l=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.removeClass("loading"),b.addClass("hidden"),j(a)||c.prop("disabled",!1)},m=function(a){return a.hasClass("loading")},n=function(a){a.attr("data-has-events",!0)},o=function(a){return!!a.attr("data-has-events")},p=function(a,b){b?n(a):o(a)||q(a)},q=function(a){a.find(g.EVENT_LIST_CONTENT).addClass("hidden"),a.find(g.EMPTY_MESSAGE).removeClass("hidden")},r=function(a,b,d){return a.removeClass("hidden"),c.render(d,{events:b}).done(function(b,d){c.appendNodeContents(a.find(g.EVENT_LIST),b,d)})},s=function(a,b){var c=b.timesort||0;return c-a},t=function(a,b,c){var d=a.attr("data-midnight"),e=+c.attr("data-start-day")*f,g=+c.attr("data-end-day")*f,h=s(d,b);return""===c.attr("data-end-day")?e<=h:e<=h&&h<g},u=function(b,c){return function(d){return t(b,d,a(c))}},v=function(b,c){var d=0,e=h.EVENT_LIST_ITEMS;return b.attr("data-course-id")&&(e=h.COURSE_EVENT_LIST_ITEMS),a.when.apply(a,a.map(b.find(g.EVENT_LIST_GROUP_CONTAINER),function(f){var g=c.filter(u(b,f));return g.length?(d+=g.length,r(a(f),g,e)):null})).then(function(){return d})},w=function(c,d){c=a(c);var g=+c.attr("data-limit"),h=+c.attr("data-course-id"),j=c.attr("data-last-id"),n=c.attr("data-midnight"),o=n-14*f;if(m(c))return a.Deferred().resolve();if(k(c),"undefined"==typeof d){var q={starttime:o,limit:g};j&&(q.aftereventid=j),h?(q.courseid=h,d=e.queryByCourse(q)):d=e.queryByTime(q)}return d.then(function(a){if(!a.events.length)return i(c),0;var b=a.events;return c.attr("data-last-id",b[b.length-1].id),b.length<g&&i(c),v(c,b).then(function(a){return a<b.length&&i(c),b.length})}).then(function(a){return p(c,a)}).fail(b.exception).always(function(){l(c)})},x=function(a){d.define(a,[d.events.activate]),a.on(d.events.activate,g.VIEW_MORE_BUTTON,function(){w(a)})};return{init:function(b){b=a(b),w(b),x(b)},registerEventListeners:x,load:w,rootSelector:g.ROOT}}); \ No newline at end of file diff --git a/myoverview/amd/build/event_list_by_course.min.js b/myoverview/amd/build/event_list_by_course.min.js new file mode 100644 index 0000000..055a8b3 --- /dev/null +++ b/myoverview/amd/build/event_list_by_course.min.js @@ -0,0 +1 @@ +define(["jquery","block_myoverview/event_list","block_myoverview/calendar_events_repository"],function(a,b,c){var d=86400,e={EVENTS_BY_COURSE_CONTAINER:'[data-region="course-events-container"]',EVENT_LIST_CONTAINER:'[data-region="event-list-container"]'},f=function(f){var g=f.find(e.EVENTS_BY_COURSE_CONTAINER);if(g.length){var h=g.find(e.EVENT_LIST_CONTAINER).first(),i=h.attr("data-midnight"),j=i-14*d,k=h.attr("data-limit"),l=g.map(function(){return a(this).attr("data-course-id")}).get(),m=c.queryByCourses({courseids:l,starttime:j,limit:k});g.each(function(c,d){d=a(d);var e=d.attr("data-course-id"),f=d.find(b.rootSelector),g=a.Deferred();m.done(function(a){var b=[],c=a.groupedbycourse.filter(function(a){return a.courseid==e});c.length&&(b=c[0].events),g.resolve({events:b})}).fail(function(a){g.reject(a)}),b.load(f,g)})}};return{init:function(b){b=a(b),f(b)}}}); \ No newline at end of file diff --git a/myoverview/amd/build/paging_bar.min.js b/myoverview/amd/build/paging_bar.min.js new file mode 100644 index 0000000..40e8b6d --- /dev/null +++ b/myoverview/amd/build/paging_bar.min.js @@ -0,0 +1 @@ +define(["jquery","core/custom_interaction_events"],function(a,b){var c={ROOT:'[data-region="paging-bar"]',PAGE_ITEM:'[data-region="page-item"]',ACTIVE_PAGE_ITEM:'[data-region="page-item"].active'},d={PAGE_SELECTED:"block_myoverview-paging-bar-page-selected"},e=function(a,b){return a.find(c.PAGE_ITEM+'[data-page-number="'+b+'"]')},f=function(a,b){var c=b.attr("data-page-number");return"first"==c?c=1:"last"==c&&(c=a.attr("data-page-count")),c},g=function(g){g=a(g),b.define(g,[b.events.activate]),g.on(b.events.activate,c.PAGE_ITEM,function(b,h){var i=a(b.target).closest(c.PAGE_ITEM),j=g.find(c.ACTIVE_PAGE_ITEM),k=f(g,i),l=k==f(g,j);l||(g.find(c.PAGE_ITEM).removeClass("active"),e(g,k).addClass("active")),g.trigger(d.PAGE_SELECTED,[{pageNumber:k,isSamePage:l}]),h.originalEvent.preventDefault()})};return{registerEventListeners:g,events:d,rootSelector:c.ROOT}}); \ No newline at end of file diff --git a/myoverview/amd/build/paging_content.min.js b/myoverview/amd/build/paging_content.min.js new file mode 100644 index 0000000..97da2ca --- /dev/null +++ b/myoverview/amd/build/paging_content.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates","block_myoverview/paging_bar"],function(a,b,c){var d={ROOT:'[data-region="paging-content"]',PAGE_REGION:'[data-region="paging-content-item"]'},e=function(b,c){this.root=a(b),this.pagingBar=a(c)};return e.rootSelector=d.ROOT,e.prototype.createPage=function(a){return this.loadContent(a).then(function(a,c){b.appendNodeContents(this.root,a,c)}.bind(this)).then(function(){return this.findPage(a)}.bind(this))},e.prototype.findPage=function(a){return this.root.find('[data-page="'+a+'"]')},e.prototype.showPage=function(a){var b=this.findPage(a);this.root.find(d.PAGE_REGION).addClass("hidden"),b.length?b.removeClass("hidden"):this.createPage(a).done(function(a){a.removeClass("hidden")})},e.prototype.registerEventListeners=function(){this.pagingBar.on(c.events.PAGE_SELECTED,function(a,b){b.isSamePage||this.showPage(b.pageNumber)}.bind(this))},e}); \ No newline at end of file diff --git a/myoverview/amd/build/tab_preferences.min.js b/myoverview/amd/build/tab_preferences.min.js new file mode 100644 index 0000000..da5bd97 --- /dev/null +++ b/myoverview/amd/build/tab_preferences.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/custom_interaction_events","core/notification"],function(a,b,c,d){var e=function(e){c.define(e,[c.events.activate]),e.on(c.events.activate,"[data-toggle='tab']",function(c){var e=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+e);var f={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:e}]}};b.call([f])[0].fail(d.exception)})};return{registerEventListeners:e}}); \ No newline at end of file diff --git a/myoverview/amd/src/calendar_events_repository.js b/myoverview/amd/src/calendar_events_repository.js new file mode 100644 index 0000000..64c06e0 --- /dev/null +++ b/myoverview/amd/src/calendar_events_repository.js @@ -0,0 +1,168 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 javascript module to retrieve calendar events from the server. + * + * @module block_myoverview/calendar_events_repository + * @class repository + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + var DEFAULT_LIMIT = 20; + + /** + * Retrieve a list of calendar events for the logged in user for the + * given course. + * + * Valid args are: + * int courseid Only get events for this course + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByCourse + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourse = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_course', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the given courses for the + * logged in user. + * + * Valid args are: + * array courseids Get events for these courses + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * + * @method queryByCourses + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourses = function(args) { + if (!args.hasOwnProperty('limit')) { + // This is intentionally smaller than the default limit. + args.limit = 10; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_courses', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the logged in user after the given + * time. + * + * Valid args are: + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByTime + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByTime = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_timesort', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + return { + queryByTime: queryByTime, + queryByCourse: queryByCourse, + queryByCourses: queryByCourses, + }; +}); diff --git a/myoverview/amd/src/event_list.js b/myoverview/amd/src/event_list.js new file mode 100644 index 0000000..9d96fc5 --- /dev/null +++ b/myoverview/amd/src/event_list.js @@ -0,0 +1,414 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 load and render the list of calendar events for a + * given day range. + * + * @module block_myoverview/event_list + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/notification', 'core/templates', + 'core/custom_interaction_events', + 'block_myoverview/calendar_events_repository'], + function($, Notification, Templates, CustomEvents, CalendarEventsRepository) { + + var SECONDS_IN_DAY = 60 * 60 * 24; + + var SELECTORS = { + EMPTY_MESSAGE: '[data-region="empty-message"]', + ROOT: '[data-region="event-list-container"]', + EVENT_LIST: '[data-region="event-list"]', + EVENT_LIST_CONTENT: '[data-region="event-list-content"]', + EVENT_LIST_GROUP_CONTAINER: '[data-region="event-list-group-container"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + VIEW_MORE_BUTTON: '[data-action="view-more"]' + }; + + var TEMPLATES = { + EVENT_LIST_ITEMS: 'block_myoverview/event-list-items', + COURSE_EVENT_LIST_ITEMS: 'block_myoverview/course-event-list-items' + }; + + /** + * Set a flag on the element to indicate that it has completed + * loading all event data. + * + * @method setLoadedAll + * @private + * @param {object} root The container element + */ + var setLoadedAll = function(root) { + root.attr('data-loaded-all', true); + }; + + /** + * Check if all event data has finished loading. + * + * @method hasLoadedAll + * @private + * @param {object} root The container element + * @return {bool} if the element has completed all loading + */ + var hasLoadedAll = function(root) { + return !!root.attr('data-loaded-all'); + }; + + /** + * Set the element state to loading. + * + * @method startLoading + * @private + * @param {object} root The container element + */ + var startLoading = function(root) { + var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER), + viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON); + + root.addClass('loading'); + loadingIcon.removeClass('hidden'); + viewMoreButton.prop('disabled', true); + }; + + /** + * Remove the loading state from the element. + * + * @method stopLoading + * @private + * @param {object} root The container element + */ + var stopLoading = function(root) { + var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER), + viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON); + + root.removeClass('loading'); + loadingIcon.addClass('hidden'); + + if (!hasLoadedAll(root)) { + // Only enable the button if we've got more events to load. + viewMoreButton.prop('disabled', false); + } + }; + + /** + * Check if the element is currently loading some event data. + * + * @method isLoading + * @private + * @param {object} root The container element + * @returns {Boolean} + */ + var isLoading = function(root) { + return root.hasClass('loading'); + }; + + /** + * Flag the root element to remember that it contains events. + * + * @method setHasContent + * @private + * @param {object} root The container element + */ + var setHasContent = function(root) { + root.attr('data-has-events', true); + }; + + /** + * Check if the root element has had events loaded. + * + * @method hasContent + * @private + * @param {object} root The container element + * @return {bool} + */ + var hasContent = function(root) { + return root.attr('data-has-events') ? true : false; + }; + + /** + * Update the visibility of the content area. The content area + * is hidden if we have no events. + * + * @method updateContentVisibility + * @private + * @param {object} root The container element + * @param {int} eventCount A count of the events we just received. + */ + var updateContentVisibility = function(root, eventCount) { + if (eventCount) { + // We've rendered some events, let's remember that. + setHasContent(root); + } else { + // If this is the first time trying to load events and + // we don't have any then there isn't any so let's show + // the empty message. + if (!hasContent(root)) { + hideContent(root); + } + } + }; + + /** + * Hide the content area and display the empty content message. + * + * @method hideContent + * @private + * @param {object} root The container element + */ + var hideContent = function(root) { + root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden'); + root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Render a group of calendar events and add them to the event + * list. + * + * @method renderGroup + * @private + * @param {object} group The group container element + * @param {array} calendarEvents The list of calendar events + * @param {string} templateName The template name + * @return {promise} Resolved when the elements are attached to the DOM + */ + var renderGroup = function(group, calendarEvents, templateName) { + + group.removeClass('hidden'); + + return Templates.render( + templateName, + {events: calendarEvents} + ).done(function(html, js) { + Templates.appendNodeContents(group.find(SELECTORS.EVENT_LIST), html, js); + }); + }; + + /** + * Determine the time (in seconds) from the given timestamp until the calendar + * event will need actioning. + * + * @method timeUntilEvent + * @private + * @param {int} timestamp The time to compare with + * @param {object} event The calendar event + * @return {int} + */ + var timeUntilEvent = function(timestamp, event) { + var orderTime = event.timesort || 0; + return orderTime - timestamp; + }; + + /** + * Check if the given calendar event should be added to the given event + * list group container. The event list group container will specify a + * day range for the time boundary it is interested in. + * + * If only a start day is specified for the container then it will be treated + * as an open catchment for all events that begin after that time. + * + * @method eventBelongsInContainer + * @private + * @param {object} root The root element + * @param {object} event The calendar event + * @param {object} container The group event list container + * @return {bool} + */ + var eventBelongsInContainer = function(root, event, container) { + var todayTime = root.attr('data-midnight'), + timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY, + timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY, + timeUntilEventNeedsAction = timeUntilEvent(todayTime, event); + + if (container.attr('data-end-day') === '') { + return timeUntilContainerStart <= timeUntilEventNeedsAction; + } else { + return timeUntilContainerStart <= timeUntilEventNeedsAction && + timeUntilEventNeedsAction < timeUntilContainerEnd; + } + }; + + /** + * Return a function that can be used to filter a list of events based on the day + * range specified on the given event list group container. + * + * @method getFilterCallbackForContainer + * @private + * @param {object} root The root element + * @param {object} container Event list group container + * @return {function} + */ + var getFilterCallbackForContainer = function(root, container) { + return function(event) { + return eventBelongsInContainer(root, event, $(container)); + }; + }; + + /** + * Render the given calendar events in the container element. The container + * elements must have a day range defined using data attributes that will be + * used to group the calendar events according to their order time. + * + * @method render + * @private + * @param {object} root The container element + * @param {array} calendarEvents A list of calendar events + * @return {promise} Resolved with a count of the number of rendered events + */ + var render = function(root, calendarEvents) { + var renderCount = 0; + var templateName = TEMPLATES.EVENT_LIST_ITEMS; + + if (root.attr('data-course-id')) { + templateName = TEMPLATES.COURSE_EVENT_LIST_ITEMS; + } + + // Loop over each of the element list groups and find the set of calendar events + // that belong to that group (as defined by the group's day range). The matching + // list of calendar events are rendered and added to the DOM within that group. + return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) { + var events = calendarEvents.filter(getFilterCallbackForContainer(root, container)); + + if (events.length) { + renderCount += events.length; + return renderGroup($(container), events, templateName); + } else { + return null; + } + })).then(function() { + return renderCount; + }); + }; + + /** + * Retrieve a list of calendar events, render and append them to the end of the + * existing list. The events will be loaded based on the set of data attributes + * on the root element. + * + * This function can be provided with a jQuery promise. If it is then it won't + * attempt to load data by itself, instead it will use the given promise. + * + * The provided promise must resolve with an an object that has an events key + * and value is an array of calendar events. + * E.g. + * { events: ['event 1', 'event 2'] } + * + * @method load + * @param {object} root The root element of the event list + * @param {object} promise A jQuery promise resolved with events + * @return {promise} A jquery promise + */ + var load = function(root, promise) { + root = $(root); + var limit = +root.attr('data-limit'), + courseId = +root.attr('data-course-id'), + lastId = root.attr('data-last-id'), + midnight = root.attr('data-midnight'), + startTime = midnight - (14 * SECONDS_IN_DAY); + + // Don't load twice. + if (isLoading(root)) { + return $.Deferred().resolve(); + } + + startLoading(root); + + // If we haven't been provided a promise to resolve the + // data then we will load our own. + if (typeof promise == 'undefined') { + var args = { + starttime: startTime, + limit: limit, + }; + + if (lastId) { + args.aftereventid = lastId; + } + + // If we have a course id then we only want events from that course. + if (courseId) { + args.courseid = courseId; + promise = CalendarEventsRepository.queryByCourse(args); + } else { + // Otherwise we want events from any course. + promise = CalendarEventsRepository.queryByTime(args); + } + } + + // Request data from the server. + return promise.then(function(result) { + if (!result.events.length) { + // No events, nothing to do. + setLoadedAll(root); + return 0; + } + + var calendarEvents = result.events; + + // Remember the last id we've seen. + root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id); + + if (calendarEvents.length < limit) { + // No more events to load, disable loading button. + setLoadedAll(root); + } + + // Render the events. + return render(root, calendarEvents).then(function(renderCount) { + if (renderCount < calendarEvents.length) { + // If the number of events that was rendered is less than + // the number we sent for rendering we can assume that there + // are no groups to add them in. Since the ordering of the + // events is guaranteed it means that any future requests will + // also yield events that can't be rendered, so let's not bother + // sending any more requests. + setLoadedAll(root); + } + return calendarEvents.length; + }); + }).then(function(eventCount) { + return updateContentVisibility(root, eventCount); + }).fail( + Notification.exception + ).always(function() { + stopLoading(root); + }); + }; + + /** + * Register the event listeners for the container element. + * + * @method registerEventListeners + * @param {object} root The root element of the event list + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + root.on(CustomEvents.events.activate, SELECTORS.VIEW_MORE_BUTTON, function() { + load(root); + }); + }; + + return { + init: function(root) { + root = $(root); + load(root); + registerEventListeners(root); + }, + registerEventListeners: registerEventListeners, + load: load, + rootSelector: SELECTORS.ROOT, + }; +}); diff --git a/myoverview/amd/src/event_list_by_course.js b/myoverview/amd/src/event_list_by_course.js new file mode 100644 index 0000000..32d52cc --- /dev/null +++ b/myoverview/amd/src/event_list_by_course.js @@ -0,0 +1,108 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 load and render the list of calendar events grouping by course. + * + * @module block_myoverview/events_by_course_list + * @package block_myoverview + * @copyright 2016 Simey Lameze <simey@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'block_myoverview/event_list', + 'block_myoverview/calendar_events_repository' +], +function($, EventList, EventsRepository) { + + var SECONDS_IN_DAY = 60 * 60 * 24; + + var SELECTORS = { + EVENTS_BY_COURSE_CONTAINER: '[data-region="course-events-container"]', + EVENT_LIST_CONTAINER: '[data-region="event-list-container"]', + }; + + /** + * Loop through course events containers and load calendar events for that course. + * + * @method load + * @param {Object} root The root element of sort by course list. + */ + var load = function(root) { + var courseBlocks = root.find(SELECTORS.EVENTS_BY_COURSE_CONTAINER); + + if (!courseBlocks.length) { + return; + } + + var eventList = courseBlocks.find(SELECTORS.EVENT_LIST_CONTAINER).first(); + var midnight = eventList.attr('data-midnight'); + var startTime = midnight - (14 * SECONDS_IN_DAY); + var limit = eventList.attr('data-limit'); + var courseIds = courseBlocks.map(function() { + return $(this).attr('data-course-id'); + }).get(); + + // Load the first set of events for each course in a single request. + // We want to avoid sending an individual request for each course because + // there could be lots of them. + var coursesPromise = EventsRepository.queryByCourses({ + courseids: courseIds, + starttime: startTime, + limit: limit + }); + + // Load the events into each course block. + courseBlocks.each(function(index, container) { + container = $(container); + var courseId = container.attr('data-course-id'); + var eventListContainer = container.find(EventList.rootSelector); + var promise = $.Deferred(); + + // Once all of the course events have been loaded then we need + // to extract just the ones relevant to this course block and + // hand them to the event list to render. + coursesPromise.done(function(result) { + var events = []; + // Get this course block's events from the collection returned + // from the server. + var courseGroup = result.groupedbycourse.filter(function(group) { + return group.courseid == courseId; + }); + + if (courseGroup.length) { + events = courseGroup[0].events; + } + + promise.resolve({events: events}); + }).fail(function(e) { + promise.reject(e); + }); + + // Provide the event list with a promise that will be resolved + // when we have received the events from the server. + EventList.load(eventListContainer, promise); + }); + }; + + return { + init: function(root) { + root = $(root); + load(root); + } + }; +}); diff --git a/myoverview/amd/src/paging_bar.js b/myoverview/amd/src/paging_bar.js new file mode 100644 index 0000000..e153e2d --- /dev/null +++ b/myoverview/amd/src/paging_bar.js @@ -0,0 +1,102 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 load and render the paging bar. + * + * @module block_myoverview/paging_bar + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/custom_interaction_events'], + function($, CustomEvents) { + + var SELECTORS = { + ROOT: '[data-region="paging-bar"]', + PAGE_ITEM: '[data-region="page-item"]', + ACTIVE_PAGE_ITEM: '[data-region="page-item"].active' + }; + + var EVENTS = { + PAGE_SELECTED: 'block_myoverview-paging-bar-page-selected', + }; + + /** + * Get the page element by number. + * + * @param {object} root The root element. + * @param {Number} pageNumber The page number. + * @returns {*} + */ + var getPageByNumber = function(root, pageNumber) { + return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]'); + }; + + /** + * Get the page number. + * + * @param {object} root The root element. + * @param {object} page The page. + * @returns {*} the page number + */ + var getPageNumber = function(root, page) { + var pageNumber = page.attr('data-page-number'); + + if (pageNumber == 'first') { + pageNumber = 1; + } else if (pageNumber == 'last') { + pageNumber = root.attr('data-page-count'); + } + + return pageNumber; + }; + + /** + * Register event listeners for the module. + * @param {object} root The root element. + */ + var registerEventListeners = function(root) { + root = $(root); + CustomEvents.define(root, [ + CustomEvents.events.activate + ]); + + root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) { + var page = $(e.target).closest(SELECTORS.PAGE_ITEM); + var activePage = root.find(SELECTORS.ACTIVE_PAGE_ITEM); + var pageNumber = getPageNumber(root, page); + var isSamePage = pageNumber == getPageNumber(root, activePage); + + if (!isSamePage) { + root.find(SELECTORS.PAGE_ITEM).removeClass('active'); + getPageByNumber(root, pageNumber).addClass('active'); + } + + root.trigger(EVENTS.PAGE_SELECTED, [{ + pageNumber: pageNumber, + isSamePage: isSamePage, + }]); + + data.originalEvent.preventDefault(); + }); + }; + + return { + registerEventListeners: registerEventListeners, + events: EVENTS, + rootSelector: SELECTORS.ROOT, + }; +}); diff --git a/myoverview/amd/src/paging_content.js b/myoverview/amd/src/paging_content.js new file mode 100644 index 0000000..1e33dae --- /dev/null +++ b/myoverview/amd/src/paging_content.js @@ -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/>. + +/** + * Paging content module. + * + * @module block_myoverview/paging_content + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/templates', 'block_myoverview/paging_bar'], + function($, Templates, PagingBar) { + + var SELECTORS = { + ROOT: '[data-region="paging-content"]', + PAGE_REGION: '[data-region="paging-content-item"]' + }; + + /** + * Constructor of the paging content module. + * + * @param {object} root + * @param {object} pagingBarElement + * @constructor + */ + var PagingContent = function(root, pagingBarElement) { + this.root = $(root); + this.pagingBar = $(pagingBarElement); + + }; + + PagingContent.rootSelector = SELECTORS.ROOT; + + /** + * Load content and create page. + * + * @param {Number} pageNumber + * @returns {*|Promise} + */ + PagingContent.prototype.createPage = function(pageNumber) { + + return this.loadContent(pageNumber).then(function(html, js) { + Templates.appendNodeContents(this.root, html, js); + }.bind(this)).then(function() { + return this.findPage(pageNumber); + }.bind(this) + ); + }; + + /** + * Find a page by the number. + * + * @param {Number} pageNumber The number of the page to be found. + * @returns {*} Page root + */ + PagingContent.prototype.findPage = function(pageNumber) { + return this.root.find('[data-page="' + pageNumber + '"]'); + }; + + /** + * Make a page visible. + * + * @param {Number} pageNumber The number of the page to be visible. + */ + PagingContent.prototype.showPage = function(pageNumber) { + + var existingPage = this.findPage(pageNumber); + this.root.find(SELECTORS.PAGE_REGION).addClass('hidden'); + + if (existingPage.length) { + existingPage.removeClass('hidden'); + } else { + this.createPage(pageNumber).done(function(newPage) { + newPage.removeClass('hidden'); + }); + } + }; + + /** + * Event listeners. + */ + PagingContent.prototype.registerEventListeners = function() { + + this.pagingBar.on(PagingBar.events.PAGE_SELECTED, function(e, data) { + if (!data.isSamePage) { + this.showPage(data.pageNumber); + } + }.bind(this)); + }; + + return PagingContent; +}); diff --git a/myoverview/amd/src/tab_preferences.js b/myoverview/amd/src/tab_preferences.js new file mode 100644 index 0000000..25ac2ee --- /dev/null +++ b/myoverview/amd/src/tab_preferences.js @@ -0,0 +1,61 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 used to save the user's tab preference. + * + * @package block_myoverview + * @copyright 2017 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/custom_interaction_events', + 'core/notification'], function($, Ajax, CustomEvents, Notification) { + + /** + * Registers an event that saves the user's tab preference when switching between them. + * + * @param {object} root The container element + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) { + var tabname = $(e.currentTarget).data('tabname'); + // Bootstrap does not change the URL when using BS tabs, so need to do this here. + // Also check to make sure the browser supports the history API. + if (typeof window.history.pushState === "function") { + window.history.pushState(null, null, '?myoverviewtab=' + tabname); + } + var request = { + methodname: 'core_user_update_user_preferences', + args: { + preferences: [ + { + type: 'block_myoverview_last_tab', + value: tabname + } + ] + } + }; + + Ajax.call([request])[0] + .fail(Notification.exception); + }); + }; + + return { + registerEventListeners: registerEventListeners + }; +}); diff --git a/myoverview/block_myoverview.php b/myoverview/block_myoverview.php new file mode 100644 index 0000000..8afd4a1 --- /dev/null +++ b/myoverview/block_myoverview.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/>. + +/** + * Contains the class for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * My overview block class. + * + * @package block_myoverview + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_myoverview extends block_base { + + /** + * Init. + */ + public function init() { + $this->title = get_string('pluginname', 'block_myoverview'); + } + + /** + * Returns the contents. + * + * @return stdClass contents of block + */ + public function get_content() { + if (isset($this->content)) { + return $this->content; + } + + // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference. + if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) { + // Check if the user has no preference, if so get the site setting. + if (!$tab = get_user_preferences('block_myoverview_last_tab')) { + $config = get_config('block_myoverview'); + $tab = $config->defaulttab; + } + } + + $renderable = new \block_myoverview\output\main($tab); + $renderer = $this->page->get_renderer('block_myoverview'); + + $this->content = new stdClass(); + $this->content->text = $renderer->render($renderable); + $this->content->footer = ''; + + return $this->content; + } + + /** + * Locations where block can be displayed. + * + * @return array + */ + public function applicable_formats() { + return array('my' => true); + } + + /** + * This block does contain a configuration settings. + * + * @return boolean + */ + public function has_config() { + return true; + } +} diff --git a/myoverview/classes/output/courses_view.php b/myoverview/classes/output/courses_view.php new file mode 100644 index 0000000..b00741d --- /dev/null +++ b/myoverview/classes/output/courses_view.php @@ -0,0 +1,190 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Class containing data for courses view in the myoverview block. + * + * @package block_myoverview + * @copyright 2017 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; +use core_course\external\course_summary_exporter; + +/** + * Class containing data for courses view in the myoverview block. + * + * @copyright 2017 Simey Lameze <simey@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class courses_view implements renderable, templatable { + /** Quantity of courses per page. */ + const COURSES_PER_PAGE = 6; + + /** @var array $courses List of courses the user is enrolled in. */ + protected $courses = []; + + /** @var array $coursesprogress List of progress percentage for each course. */ + protected $coursesprogress = []; + + /** + * The courses_view constructor. + * + * @param array $courses list of courses. + * @param array $coursesprogress list of courses progress. + */ + public function __construct($courses, $coursesprogress) { + $this->courses = $courses; + $this->coursesprogress = $coursesprogress; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + global $CFG; + require_once($CFG->dirroot.'/course/lib.php'); + require_once($CFG->dirroot.'/lib/coursecatlib.php'); + + // Build courses view data structure. + $coursesview = [ + 'hascourses' => !empty($this->courses) + ]; + + // How many courses we have per status? + $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0]; + foreach ($this->courses as $course) { + $courseid = $course->id; + $context = \context_course::instance($courseid); + $exporter = new course_summary_exporter($course, [ + 'context' => $context + ]); + $exportedcourse = $exporter->export($output); + // Convert summary to plain text. + $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat); + + $course = new \course_in_list($course); + foreach ($course->get_course_overviewfiles() as $file) { + $isimage = $file->is_valid_image(); + if ($isimage) { + $url = file_encode_url("$CFG->wwwroot/pluginfile.php", + '/'. $file->get_contextid(). '/'. $file->get_component(). '/'. + $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage); + $exportedcourse->courseimage = $url; + $exportedcourse->classes = 'courseimage'; + break; + } + } + + $exportedcourse->color = $this->coursecolor($course->id); + + if (!isset($exportedcourse->courseimage)) { + $pattern = new \core_geopattern(); + $pattern->setColor($exportedcourse->color); + $pattern->patternbyid($courseid); + $exportedcourse->classes = 'coursepattern'; + $exportedcourse->courseimage = $pattern->datauri(); + } + + // Include course visibility. + $exportedcourse->visible = (bool)$course->visible; + + $courseprogress = null; + + $classified = course_classify_for_timeline($course); + + if (isset($this->coursesprogress[$courseid])) { + $courseprogress = $this->coursesprogress[$courseid]['progress']; + $exportedcourse->hasprogress = !is_null($courseprogress); + $exportedcourse->progress = $courseprogress; + } + + if ($classified == COURSE_TIMELINE_PAST) { + // Courses that have already ended. + $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE); + + $coursesview['past']['pages'][$pastpages]['courses'][] = $exportedcourse; + $coursesview['past']['pages'][$pastpages]['active'] = ($pastpages == 0 ? true : false); + $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1; + $coursesview['past']['haspages'] = true; + $coursesbystatus['past']++; + } else if ($classified == COURSE_TIMELINE_FUTURE) { + // Courses that have not started yet. + $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE); + + $coursesview['future']['pages'][$futurepages]['courses'][] = $exportedcourse; + $coursesview['future']['pages'][$futurepages]['active'] = ($futurepages == 0 ? true : false); + $coursesview['future']['pages'][$futurepages]['page'] = $futurepages + 1; + $coursesview['future']['haspages'] = true; + $coursesbystatus['future']++; + } else { + // Courses still in progress. Either their end date is not set, or the end date is not yet past the current date. + $inprogresspages = floor($coursesbystatus['inprogress'] / $this::COURSES_PER_PAGE); + + $coursesview['inprogress']['pages'][$inprogresspages]['courses'][] = $exportedcourse; + $coursesview['inprogress']['pages'][$inprogresspages]['active'] = ($inprogresspages == 0 ? true : false); + $coursesview['inprogress']['pages'][$inprogresspages]['page'] = $inprogresspages + 1; + $coursesview['inprogress']['haspages'] = true; + $coursesbystatus['inprogress']++; + } + } + + // Build courses view paging bar structure. + foreach ($coursesbystatus as $status => $total) { + $quantpages = ceil($total / $this::COURSES_PER_PAGE); + + if ($quantpages) { + $coursesview[$status]['pagingbar']['disabled'] = ($quantpages <= 1); + $coursesview[$status]['pagingbar']['pagecount'] = $quantpages; + $coursesview[$status]['pagingbar']['first'] = ['page' => '«', 'url' => '#']; + $coursesview[$status]['pagingbar']['last'] = ['page' => '»', 'url' => '#']; + for ($page = 0; $page < $quantpages; $page++) { + $coursesview[$status]['pagingbar']['pages'][$page] = [ + 'number' => $page + 1, + 'page' => $page + 1, + 'url' => '#', + 'active' => ($page == 0 ? true : false) + ]; + } + } + } + + return $coursesview; + } + + /** + * Generate a semi-random color based on the courseid number (so it will always return + * the same color for a course) + * + * @param int $courseid + * @return string $color, hexvalue color code. + */ + protected function coursecolor($courseid) { + // The colour palette is hardcoded for now. It would make sense to combine it with theme settings. + $basecolors = ['#81ecec', '#74b9ff', '#a29bfe', '#dfe6e9', '#00b894', '#0984e3', '#b2bec3', '#fdcb6e', '#fd79a8', '#6c5ce7']; + + $color = $basecolors[$courseid % 10]; + return $color; + } +} diff --git a/myoverview/classes/output/main.php b/myoverview/classes/output/main.php new file mode 100644 index 0000000..2850637 --- /dev/null +++ b/myoverview/classes/output/main.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/>. + +/** + * Class containing data for my overview block. + * + * @package block_myoverview + * @copyright 2017 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; +use core_completion\progress; + +require_once($CFG->dirroot . '/blocks/myoverview/lib.php'); +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Class containing data for my overview block. + * + * @copyright 2017 Simey Lameze <simey@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class main implements renderable, templatable { + + /** + * @var string The tab to display. + */ + public $tab; + + /** + * Constructor. + * + * @param string $tab The tab to display. + */ + public function __construct($tab) { + $this->tab = $tab; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $USER; + + $courses = enrol_get_my_courses('*'); + $coursesprogress = []; + + foreach ($courses as $course) { + + $completion = new \completion_info($course); + + // First, let's make sure completion is enabled. + if (!$completion->is_enabled()) { + continue; + } + + $percentage = progress::get_course_progress_percentage($course); + if (!is_null($percentage)) { + $percentage = floor($percentage); + } + + $coursesprogress[$course->id]['completed'] = $completion->is_course_complete($USER->id); + $coursesprogress[$course->id]['progress'] = $percentage; + } + + $coursesview = new courses_view($courses, $coursesprogress); + $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out(); + $noeventsurl = $output->image_url('activities', 'block_myoverview')->out(); + + // Now, set the tab we are going to be viewing. + $viewingtimeline = false; + $viewingcourses = false; + if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) { + $viewingtimeline = true; + } else { + $viewingcourses = true; + } + + return [ + 'midnight' => usergetmidnight(time()), + 'coursesview' => $coursesview->export_for_template($output), + 'urls' => [ + 'nocourses' => $nocoursesurl, + 'noevents' => $noeventsurl + ], + 'viewingtimeline' => $viewingtimeline, + 'viewingcourses' => $viewingcourses + ]; + } +} diff --git a/myoverview/classes/output/renderer.php b/myoverview/classes/output/renderer.php new file mode 100644 index 0000000..606dd3b --- /dev/null +++ b/myoverview/classes/output/renderer.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/>. + +/** + * myoverview block rendrer + * + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die; + +use plugin_renderer_base; +use renderable; + +/** + * myoverview block renderer + * + * @package block_myoverview + * @copyright 2016 Ryan Wyllie <ryan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Return the main content for the block overview. + * + * @param main $main The main renderable + * @return string HTML string + */ + public function render_main(main $main) { + return $this->render_from_template('block_myoverview/main', $main->export_for_template($this)); + } +} diff --git a/myoverview/classes/privacy/provider.php b/myoverview/classes/privacy/provider.php new file mode 100644 index 0000000..d0ee9e8 --- /dev/null +++ b/myoverview/classes/privacy/provider.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/>. + +/** + * Privacy Subsystem implementation for block_myoverview. + * + * @package block_myoverview + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_myoverview\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_myoverview. + * + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider { + + /** + * Returns meta-data information about the myoverview block. + * + * @param \core_privacy\local\metadata\collection $collection A collection of meta-data. + * @return \core_privacy\local\metadata\collection Return the collection of meta-data. + */ + public static function get_metadata(\core_privacy\local\metadata\collection $collection) : + \core_privacy\local\metadata\collection { + $collection->add_user_preference('block_myoverview_last_tab', 'privacy:metadata:overviewlasttab'); + return $collection; + } + + /** + * Export all user preferences for the myoverview block + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences(int $userid) { + $preference = get_user_preferences('block_myoverview_last_tab', null, $userid); + if (isset($preference)) { + \core_privacy\local\request\writer::export_user_preference('block_myoverview', 'block_myoverview_last_tab', + $preference, get_string('privacy:metadata:overviewlasttab', 'block_myoverview')); + } + } +} diff --git a/myoverview/db/access.php b/myoverview/db/access.php new file mode 100644 index 0000000..d05b432 --- /dev/null +++ b/myoverview/db/access.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/>. + +/** + * Capabilities for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/myoverview:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/myoverview:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ) +); diff --git a/myoverview/lang/en/block_myoverview.php b/myoverview/lang/en/block_myoverview.php new file mode 100644 index 0000000..a3ca64e --- /dev/null +++ b/myoverview/lang/en/block_myoverview.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/>. + +/** + * Lang strings for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['defaulttab'] = 'Default tab'; +$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.'; +$string['future'] = 'Future'; +$string['inprogress'] = 'In progress'; +$string['morecourses'] = 'More courses'; +$string['myoverview:addinstance'] = 'Add a new course overview block'; +$string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard'; +$string['nocourses'] = 'No courses'; +$string['nocoursesinprogress'] = 'No in progress courses'; +$string['nocoursesfuture'] = 'No future courses'; +$string['nocoursespast'] = 'No past courses'; +$string['noevents'] = 'No upcoming activities due'; +$string['next30days'] = 'Next 30 days'; +$string['next7days'] = 'Next 7 days'; +$string['past'] = 'Past'; +$string['pluginname'] = 'Course overview'; +$string['recentlyoverdue'] = 'Recently overdue'; +$string['sortbycourses'] = 'Sort by courses'; +$string['sortbydates'] = 'Sort by dates'; +$string['timeline'] = 'Timeline'; +$string['viewcourse'] = 'View course'; +$string['viewcoursename'] = 'View course {$a}'; +$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.'; diff --git a/myoverview/lib.php b/myoverview/lib.php new file mode 100644 index 0000000..a73db25 --- /dev/null +++ b/myoverview/lib.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/>. + +/** + * Contains functions called by core. + * + * @package block_myoverview + * @copyright 2017 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * The timeline view. + */ +define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline'); + +/** + * The courses view. + */ +define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses'); + +/** + * Returns the name of the user preferences as well as the details this plugin uses. + * + * @return array + */ +function block_myoverview_user_preferences() { + $preferences = array(); + $preferences['block_myoverview_last_tab'] = array( + 'type' => PARAM_ALPHA, + 'null' => NULL_NOT_ALLOWED, + 'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW, + 'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW) + ); + + return $preferences; +} diff --git a/myoverview/pix/activities.svg b/myoverview/pix/activities.svg new file mode 100644 index 0000000..ed7546a --- /dev/null +++ b/myoverview/pix/activities.svg @@ -0,0 +1,41 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1509 148 125" preserveAspectRatio="xMinYMid meet"> + <defs> + <style> + .cls-1 { + clip-path: url(#clip-Activities); + } + + .cls-2 { + fill: #eee; + } + + .cls-3 { + fill: #c4c8cc; + } + + .cls-4 { + fill: #fff; + } + </style> + <clipPath id="clip-Activities"> + <rect x="157" y="-1509" width="148" height="125"/> + </clipPath> + </defs> + <g id="Activities" class="cls-1"> + <g id="Group_42" data-name="Group 42" transform="translate(-268 -1985)"> + <ellipse id="Ellipse_37" data-name="Ellipse 37" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/> + <rect id="Rectangle_80" data-name="Rectangle 80" class="cls-3" width="94.182" height="110.215" transform="translate(451.909 476)"/> + <g id="Group_41" data-name="Group 41" transform="translate(467.043 493)"> + <rect id="Rectangle_81" data-name="Rectangle 81" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 0.549)"/> + <rect id="Rectangle_82" data-name="Rectangle 82" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 11.652)"/> + <rect id="Rectangle_83" data-name="Rectangle 83" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 30.772)"/> + <rect id="Rectangle_84" data-name="Rectangle 84" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 41.875)"/> + <rect id="Rectangle_85" data-name="Rectangle 85" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 61.291)"/> + <rect id="Rectangle_86" data-name="Rectangle 86" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 72.393)"/> + <ellipse id="Ellipse_38" data-name="Ellipse 38" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 0)"/> + <ellipse id="Ellipse_39" data-name="Ellipse 39" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 31)"/> + <ellipse id="Ellipse_40" data-name="Ellipse 40" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 61)"/> + </g> + </g> + </g> +</svg> diff --git a/myoverview/pix/courses.svg b/myoverview/pix/courses.svg new file mode 100644 index 0000000..75e59fc --- /dev/null +++ b/myoverview/pix/courses.svg @@ -0,0 +1,52 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1305 148 125" preserveAspectRatio="xMinYMid meet"> + <defs> + <style> + .cls-1 { + clip-path: url(#clip-Courses); + } + + .cls-2 { + fill: #eee; + } + + .cls-3 { + fill: #c4c8cc; + } + + .cls-4 { + fill: #fff; + } + </style> + <clipPath id="clip-Courses"> + <rect x="157" y="-1305" width="148" height="125"/> + </clipPath> + </defs> + <g id="Courses" class="cls-1"> + <g id="Group_44" data-name="Group 44" transform="translate(-268 -1781)"> + <ellipse id="Ellipse_41" data-name="Ellipse 41" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/> + <rect id="Rectangle_87" data-name="Rectangle 87" class="cls-3" width="95.097" height="110.215" transform="translate(451.909 476)"/> + <g id="Group_43" data-name="Group 43" transform="translate(464.04 494)"> + <rect id="Rectangle_88" data-name="Rectangle 88" class="cls-4" width="31.043" height="34" transform="translate(0)"/> + <rect id="Rectangle_89" data-name="Rectangle 89" class="cls-4" width="31.043" height="34" transform="translate(0 42)"/> + <rect id="Rectangle_90" data-name="Rectangle 90" class="cls-4" width="31.067" height="34" transform="translate(39.005)"/> + <rect id="Rectangle_91" data-name="Rectangle 91" class="cls-4" width="31.067" height="34" transform="translate(39.005 42)"/> + <rect id="Rectangle_92" data-name="Rectangle 92" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 16.549)"/> + <rect id="Rectangle_93" data-name="Rectangle 93" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 58.549)"/> + <rect id="Rectangle_94" data-name="Rectangle 94" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 16.549)"/> + <rect id="Rectangle_95" data-name="Rectangle 95" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 58.549)"/> + <rect id="Rectangle_96" data-name="Rectangle 96" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 21.825)"/> + <rect id="Rectangle_97" data-name="Rectangle 97" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 26.825)"/> + <rect id="Rectangle_98" data-name="Rectangle 98" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 63.825)"/> + <rect id="Rectangle_99" data-name="Rectangle 99" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 68.825)"/> + <rect id="Rectangle_100" data-name="Rectangle 100" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 21.825)"/> + <rect id="Rectangle_101" data-name="Rectangle 101" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 26.825)"/> + <rect id="Rectangle_102" data-name="Rectangle 102" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 63.825)"/> + <rect id="Rectangle_103" data-name="Rectangle 103" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 68.825)"/> + <ellipse id="Ellipse_42" data-name="Ellipse 42" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 3.55)"/> + <ellipse id="Ellipse_43" data-name="Ellipse 43" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 45.55)"/> + <ellipse id="Ellipse_44" data-name="Ellipse 44" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 3.55)"/> + <ellipse id="Ellipse_45" data-name="Ellipse 45" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 45.55)"/> + </g> + </g> + </g> +</svg> diff --git a/myoverview/settings.php b/myoverview/settings.php new file mode 100644 index 0000000..10f084d --- /dev/null +++ b/myoverview/settings.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/>. + +/** + * Settings for the overview block. + * + * @package block_myoverview + * @copyright 2017 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot . '/blocks/myoverview/lib.php'); + +if ($ADMIN->fulltree) { + + $options = [ + BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'), + BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses') + ]; + + $settings->add(new admin_setting_configselect('block_myoverview/defaulttab', + get_string('defaulttab', 'block_myoverview'), + get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options)); +} diff --git a/myoverview/templates/course-event-list-item.mustache b/myoverview/templates/course-event-list-item.mustache new file mode 100644 index 0000000..55c0e46 --- /dev/null +++ b/myoverview/templates/course-event-list-item.mustache @@ -0,0 +1,69 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/course-event-list-item + + This template renders an event list item for the myoverview block + in the courses view. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} +<li class="list-group-item event-list-item" data-region="event-list-item"> + <div class="row"> + <div class="col-lg-7 col-xl-8"> + <div class="d-inline-block icon-large event-icon"> + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} + </div> + <div class="d-inline-block event-name-container"> + <a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a> + <p class="small text-muted text-truncate m-b-0"> + {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}} + </p> + </div> + </div> + <div class="hidden-md-down d-none d-md-block col-lg-5 col-xl-4 text-truncate"> + {{#action.actionable}} + <a href="{{{action.url}}}">{{action.name}}</a> + {{#action.itemcount}} + {{#action.showitemcount}} + <span class="tag tag-pill tag-default">{{.}}</span> + {{/action.showitemcount}} + {{/action.itemcount}} + {{/action.actionable}} + {{^action.actionable}} + <div class="text-muted">{{action.name}}</div> + {{/action.actionable}} + </div> + </div> +</li> diff --git a/myoverview/templates/course-event-list-items.mustache b/myoverview/templates/course-event-list-items.mustache new file mode 100644 index 0000000..10a1c43 --- /dev/null +++ b/myoverview/templates/course-event-list-items.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 block_myoverview/course-event-list-items + + This template renders a group of event list items for the myoverview block + sort by courses view. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +{{#events}} + {{> block_myoverview/course-event-list-item }} +{{/events}} diff --git a/myoverview/templates/course-event-list.mustache b/myoverview/templates/course-event-list.mustache new file mode 100644 index 0000000..d7f9fb2 --- /dev/null +++ b/myoverview/templates/course-event-list.mustache @@ -0,0 +1,110 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/course-event-list + + This template renders a list of events for the myoverview block + sort by courses view. + + Example context (json): + { + "urls": { + "noevents": "#" + } + } +}} +<div data-region="event-list-container" + data-limit="{{$limit}}20{{/limit}}" + data-course-id="{{$courseid}}{{/courseid}}" + data-last-id="{{$lastid}}{{/lastid}}" + data-midnight="{{midnight}}" + id="event-list-container-{{$courseid}}{{/courseid}}"> + + <div data-region="event-list-content"> + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}text-danger{{/extratitleclasses}} + {{$startday}}-14{{/startday}} + {{$endday}}0{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} today {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}0{{/startday}} + {{$endday}}1{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}1{{/startday}} + {{$endday}}7{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}7{{/startday}} + {{$endday}}30{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}30{{/startday}} + {{$endday}}{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + + <div class="text-xs-center text-center m-y-2"> + <button type="button" class="btn btn-secondary" data-action="view-more"> + {{#str}} viewmore {{/str}} + <span class="hidden" data-region="loading-icon-container"> + {{> core/loading }} + </span> + </button> + </div> + </div> + <div class="hidden text-xs-center text-center m-y-3" data-region="empty-message"> + <img class="empty-placeholder-image-sm" + src="{{urls.noevents}}" + alt="{{#str}} noevents, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p> + <a href="{{viewurl}}" class="btn btn-secondary {{#visible}}text-primary{{/visible}}" + aria-label="{{#str}} viewcoursename, block_myoverview, {{{fullnamedisplay}}} {{/str}}"> + {{#str}} viewcourse, block_myoverview {{/str}} + </a> + </div> +</div> +{{#js}} +require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#event-list-container-{{$courseid}}{{/courseid}}"); + EventList.registerEventListeners(root); +}); +{{/js}} diff --git a/myoverview/templates/course-item.mustache b/myoverview/templates/course-item.mustache new file mode 100644 index 0000000..c7ce9d8 --- /dev/null +++ b/myoverview/templates/course-item.mustache @@ -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/>. +}} +{{! + @template block_myoverview/course-item + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "shortname": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +<li class="list-group-item m-y-1"> +<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}"> + <div class="row"> + <div class="col-lg-3"> + {{> block_myoverview/course-summary }} + </div> + <div class="col-lg-9"> + {{< block_myoverview/course-event-list }} + {{$limit}}10{{/limit}} + {{$offset}}0{{/offset}} + {{$courseid}}{{id}}{{/courseid}} + {{/ block_myoverview/course-event-list }} + </div> + </div> +</div> +</li> diff --git a/myoverview/templates/course-paging-content-item.mustache b/myoverview/templates/course-paging-content-item.mustache new file mode 100644 index 0000000..bbaa637 --- /dev/null +++ b/myoverview/templates/course-paging-content-item.mustache @@ -0,0 +1,47 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/course-paging-content-item + + This template renders each course block. + + Example context (json): + { + "page": 1, + "active": true, + "courses": [ + { + "fullnamedisplay": "course 1", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + }, + { + "fullnamedisplay": "course 2", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } + ] + } +}} +{{< block_myoverview/paging-content-item }} + {{$classes}}row card-deck{{/classes}} + {{$content}} + {{#courses}} + {{> block_myoverview/courses-view-course-item }} + {{/courses}} + {{/content}} +{{/ block_myoverview/paging-content-item }} diff --git a/myoverview/templates/course-paging-content.mustache b/myoverview/templates/course-paging-content.mustache new file mode 100644 index 0000000..85ee437 --- /dev/null +++ b/myoverview/templates/course-paging-content.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/course-paging-content + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "pages": [ + { + "page": 1, + "active": true, + "courses": [ + { + "fullnamedisplay": "course 1", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + }, + { + "fullnamedisplay": "course 2", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } + ] + } + ] + } +}} +{{< block_myoverview/paging-content }} + {{$paging-content-item}} + {{> block_myoverview/course-paging-content-item }} + {{/paging-content-item}} +{{/ block_myoverview/paging-content }} diff --git a/myoverview/templates/course-summary.mustache b/myoverview/templates/course-summary.mustache new file mode 100644 index 0000000..53f40a0 --- /dev/null +++ b/myoverview/templates/course-summary.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 block_myoverview/course-summary + + This template renders the course summary (view by courses) for the myoverview block. + + Example context (json): + { + "fullnamedisplay": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +<div class="course-info-container" id="course-info-container-{{id}}"> + <div class="d-sm-none d-lg-block"> + {{> block_myoverview/progress-chart}} + <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4> + </div> + <div class="d-none d-sm-block d-lg-none visible-tablet"> + <div class="media"> + <div class="media-left pr-3"> + <div class="media-object"> + {{> block_myoverview/progress-chart}} + </div> + </div> + <div class="media-body"> + <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4> + </div> + </div> + </div> + <p class="text-muted"> + {{#shortentext}} 140, {{{summary}}}{{/shortentext}} + </p> +</div> diff --git a/myoverview/templates/courses-view-by-status.mustache b/myoverview/templates/courses-view-by-status.mustache new file mode 100644 index 0000000..3360d55 --- /dev/null +++ b/myoverview/templates/courses-view-by-status.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 block_myoverview/courses-view-by-status + + This template renders the courses view for the myoverview block. + + Example context (json): + {} +}} +<div id="{{$id}}courses-view-status-{{uniqid}}{{/id}}" + data-status="{{$status}}{{/status}}"> + + {{> block_myoverview/course-paging-content }} + + <div class="text-xs-center text-center"> + {{> block_myoverview/paging-bar }} + </div> +</div> +{{#js}} +require(['jquery', 'block_myoverview/paging_bar', 'block_myoverview/paging_content'], + function($, PagingBar, PagingContent) { + + var root = $('#{{$id}}courses-view-status-{{uniqid}}{{/id}}'); + var pagingBarElement = root.find(PagingBar.rootSelector); + var pagingContentElement = root.find(PagingContent.rootSelector); + + var content = new PagingContent(pagingContentElement, pagingBarElement); + content.registerEventListeners(); +}); +{{/js}} diff --git a/myoverview/templates/courses-view-course-item.mustache b/myoverview/templates/courses-view-course-item.mustache new file mode 100644 index 0000000..db2034d --- /dev/null +++ b/myoverview/templates/courses-view-course-item.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 block_myoverview/courses-view-course-item + + This template renders the course summary (view by courses) for the myoverview block. + + Example context (json): + { + "fullnamedisplay": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +<div class="card mb-3 courses-view-course-item"> + <a href="{{viewurl}}"> + <div class="card-img-top myoverviewimg {{classes}}" style='background-image: url("{{{courseimage}}}");'> + </div> + </a> + <div class="card-body course-info-container" id="course-info-container-{{id}}"> + + <div class="media"> + <div class="mr-2"> + {{> block_myoverview/progress-chart}} + </div> + <div class="media-body"> + <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4> + </div> + </div> + + <p class="text-muted"> + {{#shortentext}} 140, {{summary}}{{/shortentext}} + </p> + </div> +</div> \ No newline at end of file diff --git a/myoverview/templates/courses-view.mustache b/myoverview/templates/courses-view.mustache new file mode 100644 index 0000000..14ffa49 --- /dev/null +++ b/myoverview/templates/courses-view.mustache @@ -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/>. +}} +{{! + @template block_myoverview/courses-view + + This template renders the courses view for the myoverview block. + + Example context (json): + {} +}} +<div id="courses-view-{{uniqid}}" data-region="courses-view"> + {{#hascourses}} + <div class="d-flex justify-content-center"> + <ul class="nav nav-pills my-5"> + <li class="nav-item"> + <a class="nav-link active" href="#myoverview_courses_view_in_progress" data-toggle="tab"> + {{#str}} inprogress, block_myoverview {{/str}} + </a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#myoverview_courses_view_future" data-toggle="tab"> + {{#str}} future, block_myoverview {{/str}} + </a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#myoverview_courses_view_past" data-toggle="tab"> + {{#str}} past, block_myoverview {{/str}} + </a> + </li> + </ul> + </div> + <div class="tab-content"> + <div class="tab-pane active fade show" id="myoverview_courses_view_in_progress"> + {{#inprogress}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-in-progress{{/id}} + {{$status}}1{{/status}} + {{$pagingbarid}}pb-for-in-progress{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/inprogress}} + {{^inprogress}} + <div class="justify-content-center text-center mt-5"> + <img class="empty-placeholder-image-lg" + src="{{urls.nocourses}}" + alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted mt-3">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p> + </div> + {{/inprogress}} + </div> + <div class="tab-pane fade" id="myoverview_courses_view_future"> + {{#future}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-future{{/id}} + {{$status}}2{{/status}} + {{$pagingbarid}}pb-for-future{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/future}} + {{^future}} + <div class="justify-content-center text-center mt-5"> + <img class="empty-placeholder-image-lg" + src="{{urls.nocourses}}" + alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted mt-3">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p> + </div> + {{/future}} + </div> + <div class="tab-pane fade" id="myoverview_courses_view_past"> + {{#past}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-past{{/id}} + {{$status}}0{{/status}} + {{$pagingbarid}}pb-for-past{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/past}} + {{^past}} + <div class="justify-content-center text-center mt-5"> + <img class="empty-placeholder-image-lg" + src="{{urls.nocourses}}" + alt="{{#str}} nocoursespast, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted mt-3">{{#str}} nocoursespast, block_myoverview {{/str}}</p> + </div> + {{/past}} + </div> + </div> + {{/hascourses}} + {{^hascourses}} + <div class="justify-content-center text-center mt-5"> + <img class="empty-placeholder-image-lg" + src="{{urls.nocourses}}" + alt="{{#str}} nocourses, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted mt-3">{{#str}} nocourses, block_myoverview {{/str}}</p> + </div> + {{/hascourses}} +</div> \ No newline at end of file diff --git a/myoverview/templates/event-list-group.mustache b/myoverview/templates/event-list-group.mustache new file mode 100644 index 0000000..340fdcb --- /dev/null +++ b/myoverview/templates/event-list-group.mustache @@ -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/>. +}} +{{! + @template block_myoverview/event-list-group + + This template renders a list of events for the myoverview block. + + Example context (json): + { + "events": [ + { + "enddate": "Nov 4th, 10am", + "name": "Assignment due 1", + "url": "https://www.google.com", + "course": { + "fullname": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1 + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "enddate": "Nov 4th, 10am", + "name": "Assignment due 2", + "url": "https://www.google.com", + "course": { + "fullname": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1 + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +<div data-region="event-list-group-container" + data-start-day="{{$startday}}0{{/startday}}" + data-end-day="{{$endday}}{{/endday}}" + class="hidden"> + + <h5 class="h6 m-t-1 {{$extratitleclasses}}{{/extratitleclasses}}" id="event-list-title-{{uniqid}}"><strong>{{$title}}{{/title}}</strong></h5> + <ul class="list-group unstyled" data-region="event-list" aria-describedby="event-list-title-{{uniqid}}"> + {{$eventlistitems}} + {{> block_myoverview/event-list-items }} + {{/eventlistitems}} + </ul> +</div> diff --git a/myoverview/templates/event-list-item.mustache b/myoverview/templates/event-list-item.mustache new file mode 100644 index 0000000..a269b5c --- /dev/null +++ b/myoverview/templates/event-list-item.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/event-list-item + + This template renders an event list item for the myoverview block. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} +<li class="list-group-item event-list-item" data-region="event-list-item"> + <div class="row"> + <div class="col-sm-8 col-lg-6 col-xl-7"> + <div class="d-inline-block icon-large event-icon"> + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} + </div> + <div class="d-inline-block event-name-container"> + <a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a> + <p class="small text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</p> + </div> + </div> + <div class="col-sm-4 col-lg-6 col-xl-5"> + <div class="row"> + <div class="col-lg-5 text-xs-right text-lg-left text-truncate"> + {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}} + </div> + <div class="hidden-md-down d-none d-md-block col-lg-7 text-truncate"> + {{#action.actionable}} + <a href="{{{action.url}}}">{{action.name}}</a> + {{#action.itemcount}} + {{#action.showitemcount}} + <span class="tag tag-pill tag-default">{{.}}</span> + {{/action.showitemcount}} + {{/action.itemcount}} + {{/action.actionable}} + {{^action.actionable}} + <div class="text-muted">{{action.name}}</div> + {{/action.actionable}} + </div> + </div> + </div> + </div> +</li> diff --git a/myoverview/templates/event-list-items.mustache b/myoverview/templates/event-list-items.mustache new file mode 100644 index 0000000..2dc770b --- /dev/null +++ b/myoverview/templates/event-list-items.mustache @@ -0,0 +1,68 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/event-list-items + + This template renders a group of event list items for the myoverview block. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +{{#events}} + {{> block_myoverview/event-list-item }} +{{/events}} diff --git a/myoverview/templates/event-list.mustache b/myoverview/templates/event-list.mustache new file mode 100644 index 0000000..dbe3d25 --- /dev/null +++ b/myoverview/templates/event-list.mustache @@ -0,0 +1,87 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/event-list + + This template renders a list of events for the myoverview block. + + Example context (json): + { + } +}} +<div data-region="event-list-container" + data-limit="{{$limit}}20{{/limit}}" + data-course-id="{{$courseid}}{{/courseid}}" + data-last-id="{{$lastid}}{{/lastid}}" + data-midnight="{{midnight}}" + id="event-list-container-{{$courseid}}{{/courseid}}"> + + <div data-region="event-list-content"> + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}text-danger{{/extratitleclasses}} + {{$startday}}-14{{/startday}} + {{$endday}}0{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} today {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}0{{/startday}} + {{$endday}}1{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}1{{/startday}} + {{$endday}}7{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}7{{/startday}} + {{$endday}}30{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}30{{/startday}} + {{$endday}}{{/endday}} + {{/ block_myoverview/event-list-group }} + + <div class="text-xs-center text-center m-y-2"> + <button type="button" class="btn btn-secondary" data-action="view-more"> + {{#str}} viewmore {{/str}} + <span class="hidden" data-region="loading-icon-container"> + {{> core/loading }} + </span> + </button> + </div> + </div> + <div class="hidden text-xs-center text-center m-t-3" data-region="empty-message"> + <img class="empty-placeholder-image-lg" + src="{{urls.noevents}}" + alt="{{#str}} noevents, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p> + </div> +</div> +{{#js}} +require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#event-list-container-{{$courseid}}{{/courseid}}"); + EventList.registerEventListeners(root); +}); +{{/js}} diff --git a/myoverview/templates/main.mustache b/myoverview/templates/main.mustache new file mode 100644 index 0000000..e9b21bd --- /dev/null +++ b/myoverview/templates/main.mustache @@ -0,0 +1,55 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/main + + This template renders the main content area for the myoverview block. + + Example context (json): + {} +}} + +<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview"> + <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist"> + <li class="nav-item"> + <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline"> + {{#str}} timeline, block_myoverview {{/str}} + </a> + </li> + <li class="nav-item"> + <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses"> + {{#str}} courses {{/str}} + </a> + </li> + </ul> + <div class="tab-content content-centred"> + <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view"> + {{> block_myoverview/timeline-view }} + </div> + <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view"> + {{#coursesview}} + {{> block_myoverview/courses-view }} + {{/coursesview}} + </div> + </div> +</div> +{{#js}} +require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) { + var root = $('#block-myoverview-view-choices-{{uniqid}}'); + TabPreferences.registerEventListeners(root); +}); +{{/js}} diff --git a/myoverview/templates/paging-bar-item.mustache b/myoverview/templates/paging-bar-item.mustache new file mode 100644 index 0000000..955ff02 --- /dev/null +++ b/myoverview/templates/paging-bar-item.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 block_myoverview/paging-bar-item + + This template renders a single item in the paging bar. + + Example context (json): + { + "url": "#", + "number": 1, + "page": "1", + "active": true + } +}} +<li class="page-item {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}" + data-region="page-item" + data-page-number="{{$pagenumber}}{{number}}{{/pagenumber}}"> + + <a href="{{url}}" + class="page-link" + data-region="page-link"> + {{$item-content}} + {{{page}}} + {{/item-content}} + </a> +</li> diff --git a/myoverview/templates/paging-bar.mustache b/myoverview/templates/paging-bar.mustache new file mode 100644 index 0000000..71ffecf --- /dev/null +++ b/myoverview/templates/paging-bar.mustache @@ -0,0 +1,96 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/paging-bar + + This template renders the bootstrap style paging bar. + + Example context (json): + { + "pagingbar": { + "pagecount": 2, + "previous": {}, + "next": {}, + "first": { + "url": "#", + "page": "first" + }, + "last": { + "url": "#", + "page": "last" + }, + "pages": [ + { + "url": "#", + "number": 1, + "page": "1", + "active": true + }, + { + "url": "#", + "number": 2, + "page": "2" + } + ] + } + } +}} +{{#pagingbar}} +<nav aria-label="{{label}}" + id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}" + data-region="paging-bar" + data-page-count="{{pagecount}}"> + + <ul class="pagination"> + {{#previous}} + {{< block_myoverview/paging-bar-item }} + {{$item-content}} + <span aria-hidden="true">«</span> + <span class="sr-only">{{#str}}previous{{/str}}</span> + {{/item-content}} + {{/ block_myoverview/paging-bar-item }} + {{/previous}} + {{#first}} + {{< block_myoverview/paging-bar-item }} + {{$pagenumber}}first{{/pagenumber}} + {{/ block_myoverview/paging-bar-item }} + {{/first}} + {{#pages}} + {{> block_myoverview/paging-bar-item }} + {{/pages}} + {{#last}} + {{< block_myoverview/paging-bar-item }} + {{$pagenumber}}last{{/pagenumber}} + {{/ block_myoverview/paging-bar-item }} + {{/last}} + {{#next}} + {{< block_myoverview/paging-bar-item }} + {{$item-content}} + <span aria-hidden="true">»</span> + <span class="sr-only">{{#str}}next{{/str}}</span> + {{/item-content}} + {{/ block_myoverview/paging-bar-item }} + {{/next}} + </ul> +</nav> +{{#js}} +require(['jquery', 'block_myoverview/paging_bar'], function($, PagingBar) { + var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}'); + PagingBar.registerEventListeners(root); +}); +{{/js}} +{{/pagingbar}} diff --git a/myoverview/templates/paging-content-item.mustache b/myoverview/templates/paging-content-item.mustache new file mode 100644 index 0000000..82e73e1 --- /dev/null +++ b/myoverview/templates/paging-content-item.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 block_myoverview/paging-content-item + + This template renders the content of a page. It is to be used with + the paging bar to toggle visibility of the content items. + + Example context (json): + { + "active": true, + "page": 1, + "content": "<p>Some page content</p>" + } +}} +<div data-region="paging-content-item" + data-page="{{page}}" + class="{{^active}}hidden{{/active}} {{$classes}}{{/classes}}"> + {{$content}} + {{{content}}} + {{/content}} +</div> diff --git a/myoverview/templates/paging-content.mustache b/myoverview/templates/paging-content.mustache new file mode 100644 index 0000000..83a9cdd --- /dev/null +++ b/myoverview/templates/paging-content.mustache @@ -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/>. +}} +{{! + @template block_myoverview/paging-content + + This template renders each of the content regions for a paginated + content section. + + Example context (json): + { + "pages": [ + { + "active": true, + "page": 1, + "content": "<p>Some page content</p>" + }, + { + "page": 2, + "content": "<p>Some page content</p>" + } + ] + } +}} +<div id="{{$pagingcontentid}}paging-content-{{uniqid}}{{/pagingcontentid}}" data-region="paging-content"> + {{#pages}} + {{$paging-content-item}} + {{> block_myoverview/paging-content-item }} + {{/paging-content-item}} + {{/pages}} +</div> diff --git a/myoverview/templates/progress-chart.mustache b/myoverview/templates/progress-chart.mustache new file mode 100644 index 0000000..18ff2a4 --- /dev/null +++ b/myoverview/templates/progress-chart.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/progress-chart + + This template renders a doughnut chart to show course progress. + + Example context (json): + { + "hasprogress": true, + "progress": "60" + } +}} +<div class="progress-chart-container m-b-1"> + {{#hasprogress}} + <div class="progress-doughnut"> + <div class="progress-text {{#progress}}has-percent{{/progress}}">{{progress}}%</div> + <div class="progress-indicator"> + <svg xmlns="http://www.w3.org/2000/svg"> + <g> + <title aria-hidden="true">{{progress}}%</title> + <circle class="circle percent-{{progress}}" + r="27.5" + cx="35" + cy="35"/> + </g> + </svg> + </div> + </div> + {{/hasprogress}} + {{^hasprogress}} + <div class="no-progress"> + {{#pix}} i/course {{/pix}} + </div> + {{/hasprogress}} +</div> diff --git a/myoverview/templates/timeline-view-courses.mustache b/myoverview/templates/timeline-view-courses.mustache new file mode 100644 index 0000000..a569405 --- /dev/null +++ b/myoverview/templates/timeline-view-courses.mustache @@ -0,0 +1,121 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/timeline-view-courses + + This template renders the timeline view by courses for the myoverview block. + + Example context (json): + {} +}} +<div id="sort-by-courses-view-{{uniqid}}"> + {{#coursesview}} + {{#inprogress}} + {{#haspages}} + {{#pages}} + <ul class="list-group unstyled hidden" data-region="course-block"> + {{#courses}} {{> block_myoverview/course-item }} {{/courses}} + </ul> + {{/pages}} + <div class="text-xs-center text-center m-t-1"> + <button type="button" class="btn btn-secondary" data-action="more-courses"> + {{#str}} morecourses, block_myoverview {{/str}} + <span class="hidden" data-region="loading-icon-container"> + {{> core/loading }} + </span> + </button> + </div> + {{/haspages}} + {{^haspages}} + <div class="text-xs-center text-center m-t-3"> + <img class="empty-placeholder-image-lg" + src="{{urls.noevents}}" + alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p> + </div> + {{/haspages}} + {{/inprogress}} + {{^inprogress}} + <div class="text-xs-center text-center m-t-3"> + <img class="empty-placeholder-image-lg" + src="{{urls.noevents}}" + alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}" + role="presentation"> + <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p> + </div> + {{/inprogress}} + {{/coursesview}} +</div> +{{#js}} + require(['jquery', 'core/custom_interaction_events', 'block_myoverview/event_list_by_course'], + function($, CustomEvents, EventListByCourse) { + + var root = $("#sort-by-courses-view-{{uniqid}}"); + // This flag is used so that we can delay the loading of the events until the tab + // is toggled by the user. + var seen = false; + + CustomEvents.define(root, [CustomEvents.events.activate]); + // Show more courses and load their events when the user clicks the "more courses" + // button. + root.on(CustomEvents.events.activate, '[data-action="more-courses"]', function(e, data) { + var button = $(e.target); + var blocks = root.find('[data-region="course-block"].hidden'); + + if (blocks && blocks.length) { + var block = blocks.first(); + EventListByCourse.init(block); + block.removeClass('hidden'); + } + + // If there was only one hidden block then we have no more to show now + // so we can disable the button. + if (blocks && blocks.length == 1) { + button.prop('disabled', true); + } + + if (data) { + data.originalEvent.preventDefault(); + data.originalEvent.stopPropagation(); + } + e.stopPropagation(); + }); + + // Listen for when the user changes tab so that we can show the first set of courses + // and load their events when they request the sort by courses view for the first time. + root.closest('[data-region="timeline-view"]').on('shown shown.bs.tab', function(e) { + if (seen) { + return; + } + + var tab = $(e.target); + var tabTarget = $(tab.attr('href')); + + if (!tabTarget || !tabTarget.length) { + return; + } + + var viewCourses = tabTarget.find('#sort-by-courses-view-{{uniqid}}'); + + if (viewCourses && viewCourses.length && !seen) { + seen = true; + viewCourses.find('[data-action="more-courses"]').trigger(CustomEvents.events.activate); + } + }); + }); +{{/js}} diff --git a/myoverview/templates/timeline-view-dates.mustache b/myoverview/templates/timeline-view-dates.mustache new file mode 100644 index 0000000..66cb8ea --- /dev/null +++ b/myoverview/templates/timeline-view-dates.mustache @@ -0,0 +1,35 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_myoverview/timeline-view-dates + + This template renders the timeline view by dates for the myoverview block. + + Example context (json): + {} +}} +<div data-region="timeline-view-dates" id="timeline-view-dates-{{uniqid}}"> + {{< block_myoverview/event-list }} + {{$limit}}20{{/limit}} + {{/ block_myoverview/event-list }} +</div> +{{#js}} + require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#timeline-view-dates-{{uniqid}}").find('[data-region="event-list-container"]'); + EventList.load(root); + }); +{{/js}} diff --git a/myoverview/templates/timeline-view.mustache b/myoverview/templates/timeline-view.mustache new file mode 100644 index 0000000..9d57cd2 --- /dev/null +++ b/myoverview/templates/timeline-view.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 block_myoverview/timeline-view + + This template renders the timeline view for the myoverview block. + + Example context (json): + {} +}} +<div id="timeline-view-{{uniqid}}" data-region="timeline-view"> + <div class="d-flex justify-content-center"> + <ul class="nav nav-pills my-5"> + <li class="nav-item"> + <a class="nav-link active" href="#myoverview_timeline_dates" data-toggle="tab"> + {{#str}} sortbydates, block_myoverview {{/str}} + </a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#myoverview_timeline_courses" data-toggle="tab"> + {{#str}} sortbycourses, block_myoverview {{/str}} + </a> + </li> + </ul> + </div> + + <div class="tab-content"> + <div class="tab-pane active fade show" id="myoverview_timeline_dates"> + {{> block_myoverview/timeline-view-dates }} + </div> + <div class="tab-pane fade" id="myoverview_timeline_courses"> + {{> block_myoverview/timeline-view-courses }} + </div> + </div> +</div> \ No newline at end of file diff --git a/myoverview/tests/behat/block_myoverview_dashboard.feature b/myoverview/tests/behat/block_myoverview_dashboard.feature new file mode 100644 index 0000000..bf6f356 --- /dev/null +++ b/myoverview/tests/behat/block_myoverview_dashboard.feature @@ -0,0 +1,72 @@ +@block @block_myoverview @javascript +Feature: The my overview block allows users to easily access their courses and see upcoming activities + In order to enable the my overview block in a course + As a student + I can add the my overview block to my dashboard + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## | + | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + + Scenario: View courses and upcoming activities on timeline view + Given I log in as "student1" + And I click on "Timeline" "link" in the "Course overview" "block" + When I click on "Sort by dates" "link" in the "Course overview" "block" + Then I should see "Next 7 days" in the "Course overview" "block" + And I should see "Test choice 1 closes" in the "Course overview" "block" + And I should see "View choices" in the "Course overview" "block" + And I should see "Test feedback 1 closes" in the "Course overview" "block" + And I should see "Answer the questions" in the "Course overview" "block" + And I should see "Future" in the "Course overview" "block" + And I should see "Test choice 3 closes" in the "Course overview" "block" + And I should see "Test feedback 3 closes" in the "Course overview" "block" + And I log out + + Scenario: Past activities should not be displayed on the timeline view + Given I log in as "student1" + And I click on "Timeline" "link" in the "Course overview" "block" + When I click on "Sort by dates" "link" in the "Course overview" "block" + And I should not see "Test choice 2 closes" in the "Course overview" "block" + And I log out + + Scenario: See the courses I am enrolled by their status on courses view + Given I log in as "student1" + And I click on "Courses" "link" in the "Course overview" "block" + And I click on "In progress" "link" in the "Course overview" "block" + And I should see "Course 2" in the "Course overview" "block" + And I should not see "Course 1" in the "Course overview" "block" + And I click on "Future" "link" in the "Course overview" "block" + And I should see "Course 3" in the "Course overview" "block" + And I should not see "Course 1" in the "Course overview" "block" + When I click on "Past" "link" in the "Course overview" "block" + Then I should see "Course 1" in the "Course overview" "block" + And I should not see "Course 2" in the "Course overview" "block" + And I should not see "Course 3" in the "Course overview" "block" + And I log out + + Scenario: No activities should be displayed if the user is not enrolled + Given I log in as "student2" + And I click on "Timeline" "link" in the "Course overview" "block" + And I should see "No upcoming activities" in the "Course overview" "block" + When I click on "Courses" "link" in the "Course overview" "block" + Then I should see "No courses" in the "Course overview" "block" + And I log out diff --git a/myoverview/tests/behat/block_myoverview_progress.feature b/myoverview/tests/behat/block_myoverview_progress.feature new file mode 100644 index 0000000..44cd539 --- /dev/null +++ b/myoverview/tests/behat/block_myoverview_progress.feature @@ -0,0 +1,63 @@ +@block @block_myoverview @javascript +Feature: Course overview block show users their progress on courses + In order to enable the my overview block in a course + As a student + I can see the progress percentage of the courses I am enrolled in + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | startdate | enddate | + | Course 1 | C1 | 0 | 1 | ##yesterday## | ##tomorrow## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C1 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + + Scenario: Course progress percentage should not be displayed if completion is not enabled + Given I log in as "student1" + And I click on "Timeline" "link" in the "Course overview" "block" + When I click on "Sort by courses" "link" in the "Course overview" "block" + Then I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element" + And I should not see "0%" in the "Course overview" "block" + And I click on "Courses" "link" in the "Course overview" "block" + And I click on "In progress" "link" in the "Course overview" "block" + And I should see "Course 1" in the "Course overview" "block" + And I should not see "0%" in the "Course overview" "block" + And I log out + + Scenario: User complete activity and verify his progress + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test choice 1" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + | id_completionview | 1 | + And I press "Save and return to course" + And I log out + And I log in as "student1" + And I click on "Sort by courses" "link" in the "Course overview" "block" + And I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element" + And I should see "0%" in the "Course overview" "block" + And I click on "Courses" "link" in the "Course overview" "block" + When I click on "In progress" "link" in the "Course overview" "block" + Then I should see "Course 1" in the "Course overview" "block" + And I should see "0%" in the "Course overview" "block" + And I am on "Course 1" course homepage + And I follow "Test choice 1" + And I follow "Dashboard" in the user menu + And I click on "Timeline" "link" in the "Course overview" "block" + And I click on "Sort by courses" "link" in the "Course overview" "block" + And I should see "100%" in the "Course overview" "block" + And I click on "Courses" "link" in the "Course overview" "block" + And I click on "In progress" "link" in the "Course overview" "block" + And I should see "Course 1" in the "Course overview" "block" + And I should see "100%" in the "Course overview" "block" + And I log out diff --git a/myoverview/tests/privacy_test.php b/myoverview/tests/privacy_test.php new file mode 100644 index 0000000..875dd03 --- /dev/null +++ b/myoverview/tests/privacy_test.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/>. + +/** + * Unit tests for the block_myoverview implementation of the privacy API. + * + * @package block_myoverview + * @category test + * @copyright 2018 Adrian Greeve <adriangreeve.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\writer; +use \block_myoverview\privacy\provider; + +/** + * Unit tests for the block_myoverview implementation of the privacy API. + * + * @copyright 2018 Adrian Greeve <adriangreeve.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Ensure that export_user_preferences returns no data if the user has not visited the myoverview block. + */ + public function test_export_user_preferences_no_pref() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + provider::export_user_preferences($user->id); + $writer = writer::with_context(\context_system::instance()); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Test that the preference courses is exported properly. + */ + public function test_export_user_preferences_course_preference() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + set_user_preference('block_myoverview_last_tab', 'courses', $user); + + provider::export_user_preferences($user->id); + $writer = writer::with_context(\context_system::instance()); + $blockpreferences = $writer->get_user_preferences('block_myoverview'); + $this->assertEquals('courses', $blockpreferences->block_myoverview_last_tab->value); + } + + /** + * Test that the preference timeline is exported properly. + */ + public function test_export_user_preferences_timeline_preference() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + set_user_preference('block_myoverview_last_tab', 'timeline', $user); + + provider::export_user_preferences($user->id); + $writer = writer::with_context(\context_system::instance()); + $blockpreferences = $writer->get_user_preferences('block_myoverview'); + $this->assertEquals('timeline', $blockpreferences->block_myoverview_last_tab->value); + } +} diff --git a/myoverview/version.php b/myoverview/version.php new file mode 100644 index 0000000..26ccc6d --- /dev/null +++ b/myoverview/version.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/>. + +/** + * Version details for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2018050800; // Requires this Moodle version. +$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics). diff --git a/myprofile/block_myprofile.php b/myprofile/block_myprofile.php new file mode 100644 index 0000000..b596f15 --- /dev/null +++ b/myprofile/block_myprofile.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/>. + +/** + * Block displaying information about current logged-in user. + * + * This block can be used as anti cheating measure, you + * can easily check the logged-in user matches the person + * operating the computer. + * + * @package block_myprofile + * @copyright 2010 Remote-Learner.net + * @author Olav Jordan <olav.jordan@remote-learner.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Displays the current user's profile information. + * + * @copyright 2010 Remote-Learner.net + * @author Olav Jordan <olav.jordan@remote-learner.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_myprofile extends block_base { + /** + * block initializations + */ + public function init() { + $this->title = get_string('pluginname', 'block_myprofile'); + } + + /** + * block contents + * + * @return object + */ + public function get_content() { + global $CFG, $USER, $DB, $OUTPUT, $PAGE; + + if ($this->content !== NULL) { + return $this->content; + } + + if (!isloggedin() or isguestuser()) { + return ''; // Never useful unless you are logged in as real users + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + $course = $this->page->course; + + if (!isset($this->config->display_picture) || $this->config->display_picture == 1) { + $this->content->text .= '<div class="myprofileitem picture">'; + $this->content->text .= $OUTPUT->user_picture($USER, array('courseid'=>$course->id, 'size'=>'100', 'class'=>'profilepicture')); // The new class makes CSS easier + $this->content->text .= '</div>'; + } + + $this->content->text .= '<div class="myprofileitem fullname">'.fullname($USER).'</div>'; + + if(!isset($this->config->display_country) || $this->config->display_country == 1) { + $countries = get_string_manager()->get_list_of_countries(true); + if (isset($countries[$USER->country])) { + $this->content->text .= '<div class="myprofileitem country">'; + $this->content->text .= get_string('country') . ': ' . $countries[$USER->country]; + $this->content->text .= '</div>'; + } + } + + if(!isset($this->config->display_city) || $this->config->display_city == 1) { + $this->content->text .= '<div class="myprofileitem city">'; + $this->content->text .= get_string('city') . ': ' . format_string($USER->city); + $this->content->text .= '</div>'; + } + + if(!isset($this->config->display_email) || $this->config->display_email == 1) { + $this->content->text .= '<div class="myprofileitem email">'; + $this->content->text .= obfuscate_mailto($USER->email, ''); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_icq) && !empty($USER->icq)) { + $this->content->text .= '<div class="myprofileitem icq">'; + $this->content->text .= 'ICQ: ' . s($USER->icq); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_skype) && !empty($USER->skype)) { + $this->content->text .= '<div class="myprofileitem skype">'; + $this->content->text .= 'Skype: ' . s($USER->skype); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_yahoo) && !empty($USER->yahoo)) { + $this->content->text .= '<div class="myprofileitem yahoo">'; + $this->content->text .= 'Yahoo: ' . s($USER->yahoo); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_aim) && !empty($USER->aim)) { + $this->content->text .= '<div class="myprofileitem aim">'; + $this->content->text .= 'AIM: ' . s($USER->aim); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_msn) && !empty($USER->msn)) { + $this->content->text .= '<div class="myprofileitem msn">'; + $this->content->text .= 'MSN: ' . s($USER->msn); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_phone1) && !empty($USER->phone1)) { + $this->content->text .= '<div class="myprofileitem phone1">'; + $this->content->text .= get_string('phone1').': ' . s($USER->phone1); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_phone2) && !empty($USER->phone2)) { + $this->content->text .= '<div class="myprofileitem phone2">'; + $this->content->text .= get_string('phone2').': ' . s($USER->phone2); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_institution) && !empty($USER->institution)) { + $this->content->text .= '<div class="myprofileitem institution">'; + $this->content->text .= format_string($USER->institution); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_address) && !empty($USER->address)) { + $this->content->text .= '<div class="myprofileitem address">'; + $this->content->text .= format_string($USER->address); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_firstaccess) && !empty($USER->firstaccess)) { + $this->content->text .= '<div class="myprofileitem firstaccess">'; + $this->content->text .= get_string('firstaccess').': ' . userdate($USER->firstaccess); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_lastaccess) && !empty($USER->lastaccess)) { + $this->content->text .= '<div class="myprofileitem lastaccess">'; + $this->content->text .= get_string('lastaccess').': ' . userdate($USER->lastaccess); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_currentlogin) && !empty($USER->currentlogin)) { + $this->content->text .= '<div class="myprofileitem currentlogin">'; + $this->content->text .= get_string('login').': ' . userdate($USER->currentlogin); + $this->content->text .= '</div>'; + } + + if(!empty($this->config->display_lastip) && !empty($USER->lastip)) { + $this->content->text .= '<div class="myprofileitem lastip">'; + $this->content->text .= 'IP: ' . $USER->lastip; + $this->content->text .= '</div>'; + } + + return $this->content; + } + + /** + * allow the block to have a configuration page + * + * @return boolean + */ + public function has_config() { + return false; + } + + /** + * allow more than one instance of the block on a page + * + * @return boolean + */ + public function instance_allow_multiple() { + //allow more than one instance on a page + return false; + } + + /** + * allow instances to have their own configuration + * + * @return boolean + */ + function instance_allow_config() { + //allow instances to have their own configuration + return false; + } + + /** + * instance specialisations (must have instance allow config true) + * + */ + public function specialization() { + } + + /** + * locations where block can be displayed + * + * @return array + */ + public function applicable_formats() { + return array('all'=>true); + } + + /** + * post install configurations + * + */ + public function after_install() { + } + + /** + * post delete configurations + * + */ + public function before_delete() { + } + +} diff --git a/myprofile/classes/privacy/provider.php b/myprofile/classes/privacy/provider.php new file mode 100644 index 0000000..160c186 --- /dev/null +++ b/myprofile/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_myprofile. + * + * @package block_myprofile + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_myprofile\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_myprofile implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/myprofile/db/access.php b/myprofile/db/access.php new file mode 100644 index 0000000..328a6f0 --- /dev/null +++ b/myprofile/db/access.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/>. + +/** + * My profile block caps. + * + * @package block_myprofile + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/myprofile:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/myprofile: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/myprofile/edit_form.php b/myprofile/edit_form.php new file mode 100644 index 0000000..0302647 --- /dev/null +++ b/myprofile/edit_form.php @@ -0,0 +1,151 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +defined('MOODLE_INTERNAL') || die(); + +/** + * Form for editing profile block settings + * + * @package block_myprofile + * @copyright 2010 Remote-Learner.net + * @author Olav Jordan <olav.jordan@remote-learner.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_myprofile_edit_form extends block_edit_form { + protected function specific_definition($mform) { + global $CFG; + $mform->addElement('header', 'configheader', get_string('myprofile_settings', 'block_myprofile')); + + $mform->addElement('selectyesno', 'config_display_picture', get_string('display_picture', 'block_myprofile')); + if (isset($this->block->config->display_picture)) { + $mform->setDefault('config_display_picture', $this->block->config->display_picture); + } else { + $mform->setDefault('config_display_picture', '1'); + } + + $mform->addElement('selectyesno', 'config_display_country', get_string('display_country', 'block_myprofile')); + if (isset($this->block->config->display_country)) { + $mform->setDefault('config_display_country', $this->block->config->display_country); + } else { + $mform->setDefault('config_display_country', '1'); + } + + $mform->addElement('selectyesno', 'config_display_city', get_string('display_city', 'block_myprofile')); + if (isset($this->block->config->display_city)) { + $mform->setDefault('config_display_city', $this->block->config->display_city); + } else { + $mform->setDefault('config_display_city', '1'); + } + + $mform->addElement('selectyesno', 'config_display_email', get_string('display_email', 'block_myprofile')); + if (isset($this->block->config->display_email)) { + $mform->setDefault('config_display_email', $this->block->config->display_email); + } else { + $mform->setDefault('config_display_email', '1'); + } + + $mform->addElement('selectyesno', 'config_display_icq', get_string('display_icq', 'block_myprofile')); + if (isset($this->block->config->display_icq)) { + $mform->setDefault('config_display_icq', $this->block->config->display_icq); + } else { + $mform->setDefault('config_display_icq', '0'); + } + + $mform->addElement('selectyesno', 'config_display_skype', get_string('display_skype', 'block_myprofile')); + if (isset($this->block->config->display_skype)) { + $mform->setDefault('config_display_skype', $this->block->config->display_skype); + } else { + $mform->setDefault('config_display_skype', '0'); + } + + $mform->addElement('selectyesno', 'config_display_yahoo', get_string('display_yahoo', 'block_myprofile')); + if (isset($this->block->config->display_yahoo)) { + $mform->setDefault('config_display_yahoo', $this->block->config->display_yahoo); + } else { + $mform->setDefault('config_display_yahoo', '0'); + } + + $mform->addElement('selectyesno', 'config_display_aim', get_string('display_aim', 'block_myprofile')); + if (isset($this->block->config->display_aim)) { + $mform->setDefault('config_display_aim', $this->block->config->display_aim); + } else { + $mform->setDefault('config_display_aim', '0'); + } + + $mform->addElement('selectyesno', 'config_display_msn', get_string('display_msn', 'block_myprofile')); + if (isset($this->block->config->display_msn)) { + $mform->setDefault('config_display_msn', $this->block->config->display_msn); + } else { + $mform->setDefault('config_display_msn', '0'); + } + + $mform->addElement('selectyesno', 'config_display_phone1', get_string('display_phone1', 'block_myprofile')); + if (isset($this->block->config->display_phone1)) { + $mform->setDefault('config_display_phone1', $this->block->config->display_phone1); + } else { + $mform->setDefault('config_display_phone1', '0'); + } + + $mform->addElement('selectyesno', 'config_display_phone2', get_string('display_phone2', 'block_myprofile')); + if (isset($this->block->config->display_phone2)) { + $mform->setDefault('config_display_phone2', $this->block->config->display_phone2); + } else { + $mform->setDefault('config_display_phone2', '0'); + } + + $mform->addElement('selectyesno', 'config_display_institution', get_string('display_institution', 'block_myprofile')); + if (isset($this->block->config->display_institution)) { + $mform->setDefault('config_display_institution', $this->block->config->display_institution); + } else { + $mform->setDefault('config_display_institution', '0'); + } + + $mform->addElement('selectyesno', 'config_display_address', get_string('display_address', 'block_myprofile')); + if (isset($this->block->config->display_address)) { + $mform->setDefault('config_display_address', $this->block->config->display_address); + } else { + $mform->setDefault('config_display_address', '0'); + } + + $mform->addElement('selectyesno', 'config_display_firstaccess', get_string('display_firstaccess', 'block_myprofile')); + if (isset($this->block->config->display_firstaccess)) { + $mform->setDefault('config_display_firstaccess', $this->block->config->display_firstaccess); + } else { + $mform->setDefault('config_display_firstaccess', '0'); + } + + $mform->addElement('selectyesno', 'config_display_lastaccess', get_string('display_lastaccess', 'block_myprofile')); + if (isset($this->block->config->display_lastaccess)) { + $mform->setDefault('config_display_lastaccess', $this->block->config->display_lastaccess); + } else { + $mform->setDefault('config_display_lastaccess', '0'); + } + + $mform->addElement('selectyesno', 'config_display_currentlogin', get_string('display_currentlogin', 'block_myprofile')); + if (isset($this->block->config->display_currentlogin)) { + $mform->setDefault('config_display_currentlogin', $this->block->config->display_currentlogin); + } else { + $mform->setDefault('config_display_currentlogin', '0'); + } + + $mform->addElement('selectyesno', 'config_display_lastip', get_string('display_lastip', 'block_myprofile')); + if (isset($this->block->config->display_lastip)) { + $mform->setDefault('config_display_lastip', $this->block->config->display_lastip); + } else { + $mform->setDefault('config_display_lastip', '0'); + } + } +} \ No newline at end of file diff --git a/myprofile/lang/en/block_myprofile.php b/myprofile/lang/en/block_myprofile.php new file mode 100644 index 0000000..b7de027 --- /dev/null +++ b/myprofile/lang/en/block_myprofile.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/>. + +/** + * Strings for component 'block_myprofile', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_myprofile + * @copyright 2010 Remote-Learner.net + * @author Olav Jordan <olav.jordan@remote-learner.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['contentsettings'] = 'Display settings for content region'; +$string['display_picture'] = 'Display picture'; +$string['display_country'] = 'Display country'; +$string['display_city'] = 'Display city'; +$string['display_email'] = 'Display email'; +$string['display_icq'] = 'Display ICQ'; +$string['display_skype'] = 'Display Skype'; +$string['display_yahoo'] = 'Display Yahoo'; +$string['display_aim'] = 'Display AIM'; +$string['display_msn'] = 'Display MSN'; +$string['display_phone1'] = 'Display phone'; +$string['display_phone2'] = 'Display mobile phone'; +$string['display_institution'] = 'Display institution'; +$string['display_address'] = 'Display address'; +$string['display_firstaccess'] = 'Display first access'; +$string['display_lastaccess'] = 'Display last access'; +$string['display_currentlogin'] = 'Display current login'; +$string['display_lastip'] = 'Display last IP'; +$string['myprofile:addinstance'] = 'Add a new logged in user block'; +$string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboard'; +$string['myprofile_settings'] = 'Visible user information'; +$string['pluginname'] = 'Logged in user'; +$string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.'; + +// Deprecated since Moodle 3.2. +$string['display_un'] = 'Display name'; diff --git a/myprofile/lang/en/deprecated.txt b/myprofile/lang/en/deprecated.txt new file mode 100644 index 0000000..e79d781 --- /dev/null +++ b/myprofile/lang/en/deprecated.txt @@ -0,0 +1 @@ +display_un,block_myprofile diff --git a/myprofile/styles.css b/myprofile/styles.css new file mode 100644 index 0000000..ee3944e --- /dev/null +++ b/myprofile/styles.css @@ -0,0 +1,14 @@ +.block_myprofile img.profilepicture { + height: 100px; + width: 100px; +} + +.block_myprofile .myprofileitem.fullname { + font-size: 1.5em; + font-weight: bold; +} + +.block_myprofile .myprofileitem.edit { + text-align: right; +} + diff --git a/myprofile/tests/behat/block_myprofile.feature b/myprofile/tests/behat/block_myprofile.feature new file mode 100644 index 0000000..8bd076c --- /dev/null +++ b/myprofile/tests/behat/block_myprofile.feature @@ -0,0 +1,309 @@ +@block @block_myprofile +Feature: The logged in user block allows users to view their profile information + In order to enable the logged in user block + As a user + I can add the logged in user block and configure it to show my information + + Scenario: Configure the logged in user block to show / hide the users country + Given the following "users" exist: + | username | firstname | lastname | email | country | + | teacher1 | Teacher | One | teacher1@example.com | AU | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display country | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "Australia" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display country | Yes | + And I press "Save changes" + And I should see "Australia" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users city + Given the following "users" exist: + | username | firstname | lastname | email | city | + | teacher1 | Teacher | One | teacher1@example.com | Perth | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display city | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "Perth" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display city | Yes | + And I press "Save changes" + And I should see "Perth" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users email + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display email | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "teacher1@example.com" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display email | Yes | + And I press "Save changes" + And I should see "teacher1@example.com" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users ICQ + Given the following "users" exist: + | username | firstname | lastname | email | icq | + | teacher1 | Teacher | One | teacher1@example.com | myicq | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display ICQ | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myicq" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display ICQ | Yes | + And I press "Save changes" + And I should see "myicq" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users Skype + Given the following "users" exist: + | username | firstname | lastname | email | skype | + | teacher1 | Teacher | One | teacher1@example.com | myskype | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display Skype | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myskype" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display Skype | Yes | + And I press "Save changes" + And I should see "myskype" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users Yahoo + Given the following "users" exist: + | username | firstname | lastname | email | yahoo | + | teacher1 | Teacher | One | teacher1@example.com | myyahoo | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display Yahoo | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myyahoo" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display Yahoo | Yes | + And I press "Save changes" + And I should see "myyahoo" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users AIM + Given the following "users" exist: + | username | firstname | lastname | email | aim | + | teacher1 | Teacher | One | teacher1@example.com | myaim | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display AIM | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myaim" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display AIM | Yes | + And I press "Save changes" + And I should see "myaim" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users MSN + Given the following "users" exist: + | username | firstname | lastname | email | msn | + | teacher1 | Teacher | One | teacher1@example.com | mymsn | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display MSN | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "mymsn" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display MSN | Yes | + And I press "Save changes" + And I should see "mymsn" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users phone + Given the following "users" exist: + | username | firstname | lastname | email | phone1 | + | teacher1 | Teacher | One | teacher1@example.com | 555-5555 | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display phone | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "555-5555" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display phone | Yes | + And I press "Save changes" + And I should see "555-5555" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users mobile phone + Given the following "users" exist: + | username | firstname | lastname | email | phone2 | + | teacher1 | Teacher | One | teacher1@example.com | 555-5555 | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display mobile phone | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "555-5555" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display mobile phone | Yes | + And I press "Save changes" + And I should see "555-5555" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users Institution + Given the following "users" exist: + | username | firstname | lastname | email | institution | + | teacher1 | Teacher | One | teacher1@example.com | myinstitution | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display institution | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myinstitution" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display institution | Yes | + And I press "Save changes" + And I should see "myinstitution" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users address + Given the following "users" exist: + | username | firstname | lastname | email | address | + | teacher1 | Teacher | One | teacher1@example.com | myaddress | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display address | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "myaddress" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display address | Yes | + And I press "Save changes" + And I should see "myaddress" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users first access + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display first access | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "First access:" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display first access | Yes | + And I press "Save changes" + And I should see "First access:" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users last access + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display last access | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "Last access:" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display last access | Yes | + And I press "Save changes" + And I should see "Last access:" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users current login + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display current login | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "Log in:" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display current login | Yes | + And I press "Save changes" + And I should see "Log in:" in the "Logged in user" "block" + + Scenario: Configure the logged in user block to show / hide the users last ip + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display last IP | No | + And I press "Save changes" + Then I should see "Teacher One" in the "Logged in user" "block" + And I should not see "IP:" in the "Logged in user" "block" + And I configure the "Logged in user" block + And I set the following fields to these values: + | Display last IP | Yes | + And I press "Save changes" + And I should see "IP:" in the "Logged in user" "block" diff --git a/myprofile/tests/behat/block_myprofile_activity.feature b/myprofile/tests/behat/block_myprofile_activity.feature new file mode 100644 index 0000000..bc0f704 --- /dev/null +++ b/myprofile/tests/behat/block_myprofile_activity.feature @@ -0,0 +1,24 @@ +@block @block_myprofile +Feature: The logged in user block allows users to view their profile information in an activity + In order to enable the logged in user block in an activity + As a teacher + I can add the logged in user block to an activity and view my information + + Scenario: View the logged in user block by a user in an activity + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | One | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | course | idnumber | name | intro | + | page | C1 | page1 | Test page name | Test page description | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + When I add the "Logged in user" block + Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/myprofile/tests/behat/block_myprofile_course.feature b/myprofile/tests/behat/block_myprofile_course.feature new file mode 100644 index 0000000..c0aa2db --- /dev/null +++ b/myprofile/tests/behat/block_myprofile_course.feature @@ -0,0 +1,20 @@ +@block @block_myprofile +Feature: The logged in user block allows users to view their profile information in a course + In order to enable the logged in user block in a course + As a teacher + I can add the logged in user block to a course and view my information + + Scenario: View the logged in user block by a user in a course + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | One | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Logged in user" block + Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/myprofile/tests/behat/block_myprofile_dashboard.feature b/myprofile/tests/behat/block_myprofile_dashboard.feature new file mode 100644 index 0000000..e130522 --- /dev/null +++ b/myprofile/tests/behat/block_myprofile_dashboard.feature @@ -0,0 +1,14 @@ +@block @block_myprofile +Feature: The logged in user block allows users to view their profile information in on the dashboard + In order to enable the logged in user block on the dashboard + As a user + I can add the logged in user block to a the dashboard and view my information + + Scenario: View the logged in user block by a user on the dashboard + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And I log in as "teacher1" + And I press "Customise this page" + When I add the "Logged in user" block + Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/myprofile/tests/behat/block_myprofile_frontpage.feature b/myprofile/tests/behat/block_myprofile_frontpage.feature new file mode 100644 index 0000000..5b14df3 --- /dev/null +++ b/myprofile/tests/behat/block_myprofile_frontpage.feature @@ -0,0 +1,25 @@ +@block @block_myprofile +Feature: The logged in user block allows users to view their profile information on the front page + In order to enable the logged in user block on the frontpage + As an admin + I can add the logged in user block to the frontpage and view my information + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | One | teacher1@example.com | T1 | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Logged in user" block + And I log out + + Scenario: Try to view the logged in user block as a guest + Given I log in as "guest" + When I am on site homepage + Then I should not see "Logged in user" + + Scenario: View the logged in user block by a logged in user + Given I log in as "teacher1" + When I am on site homepage + Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/myprofile/version.php b/myprofile/version.php new file mode 100644 index 0000000..09d06a2 --- /dev/null +++ b/myprofile/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Current user info block. + * + * @package block_myprofile + * @copyright 2010 Remote-Learner.net + * @author Olav Jordan <olav.jordan@remote-learner.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_myprofile'; // Full name of the plugin (used for diagnostics) diff --git a/navigation/amd/build/ajax_response_renderer.min.js b/navigation/amd/build/ajax_response_renderer.min.js new file mode 100644 index 0000000..02578ec --- /dev/null +++ b/navigation/amd/build/ajax_response_renderer.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates","core/notification","core/url"],function(a,b,c,d){function e(g,h){var i=a("<ul></ul>");i.attr("role","group"),i.attr("aria-hidden",!0),a.each(h,function(g,h){if("object"==typeof h){var j=a("<li></li>"),k=a("<p></p>"),l=h.id||h.key+"_tree_item",m=null,n=!(!h.expandable&&!h.haschildren);k.addClass("tree_item"),k.attr("id",l),k.attr("role","treeitem"),k.attr("tabindex","-1"),h.requiresajaxloading&&(k.attr("data-requires-ajax",!0),k.attr("data-node-id",h.id),k.attr("data-node-key",h.key),k.attr("data-node-type",h.type)),n&&(j.addClass("collapsed contains_branch"),k.attr("aria-expanded",!1),k.addClass("branch"));var o=null;if(h.link){var p=a('<a title="'+h.title+'" href="'+h.link+'"></a>');o=p,p.append('<span class="item-content-wrap">'+h.name+"</span>"),h.hidden&&p.addClass("dimmed"),k.append(p)}else{var q=a("<span></span>");o=q,q.append('<span class="item-content-wrap">'+h.name+"</span>"),h.hidden&&q.addClass("dimmed"),k.append(q)}!h.icon||n&&h.type!==f.ACTIVITY&&h.type!==f.RESOURCE||(j.addClass("item_with_icon"),k.addClass("hasicon"),h.type===f.ACTIVITY||h.type===f.RESOURCE?(m=a("<img/>"),m.attr("alt",h.icon.alt),m.attr("title",h.icon.title),m.attr("src",d.imageUrl(h.icon.pix,h.icon.component)),a.each(h.icon.classes,function(a,b){m.addClass(b)}),o.prepend(m)):("moodle"==h.icon.component&&(h.icon.component="core"),b.renderPix(h.icon.pix,h.icon.component,h.icon.title).then(function(a){o.prepend(a)})["catch"](c.exception))),j.append(k),i.append(j),h.children&&h.children.length?e(k,h.children):n&&!h.requiresajaxloading&&(j.removeClass("contains_branch"),k.addClass("emptybranch"))}}),g.parent().append(i);var j=g.attr("id")+"_group";i.attr("id",j),g.attr("aria-owns",j),g.attr("role","treeitem")}var f={ACTIVITY:40,RESOURCE:50};return{render:function(a,b){if(b.children&&b.children.length){e(a,b.children);var c=a.children("[role='treeitem']").first(),d=a.find("#"+c.attr("aria-owns"));c.attr("aria-expanded",!0),d.attr("aria-hidden",!1)}else a.parent().hasClass("contains_branch")&&(a.parent().removeClass("contains_branch"),a.addClass("emptybranch"))}}}); \ No newline at end of file diff --git a/navigation/amd/build/nav_loader.min.js b/navigation/amd/build/nav_loader.min.js new file mode 100644 index 0000000..183a19f --- /dev/null +++ b/navigation/amd/build/nav_loader.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/config","block_navigation/ajax_response_renderer"],function(a,b,c,d){function e(a){return a.closest("[data-block]").attr("data-instanceid")}var f=c.wwwroot+"/lib/ajax/getnavbranch.php";return{load:function(b){b=a(b);var g=a.Deferred(),h={elementid:b.attr("data-node-id"),id:b.attr("data-node-key"),type:b.attr("data-node-type"),sesskey:c.sesskey,instance:e(b)},i={type:"POST",dataType:"json",data:h};return a.ajax(f,i).done(function(a){d.render(b,a),g.resolve()}),g}}}); \ No newline at end of file diff --git a/navigation/amd/build/navblock.min.js b/navigation/amd/build/navblock.min.js new file mode 100644 index 0000000..2025554 --- /dev/null +++ b/navigation/amd/build/navblock.min.js @@ -0,0 +1 @@ +define(["jquery","core/tree"],function(a,b){return{init:function(a){var c=new b(".block_navigation .block_tree");c.finishExpandingGroup=function(c){b.prototype.finishExpandingGroup.call(this,c),Y.use("moodle-core-event",function(){Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED,{instanceid:a})})},c.collapseGroup=function(c){b.prototype.collapseGroup.call(this,c),Y.use("moodle-core-event",function(){Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED,{instanceid:a})})}}}}); \ No newline at end of file diff --git a/navigation/amd/build/site_admin_loader.min.js b/navigation/amd/build/site_admin_loader.min.js new file mode 100644 index 0000000..9bd76cb --- /dev/null +++ b/navigation/amd/build/site_admin_loader.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/config","block_navigation/ajax_response_renderer"],function(a,b,c,d){var e=71,f=c.wwwroot+"/lib/ajax/getsiteadminbranch.php";return{load:function(b){b=a(b);var g=a.Deferred(),h={type:e,sesskey:c.sesskey},i={type:"POST",dataType:"json",data:h};return a.ajax(f,i).done(function(a){d.render(b,a),g.resolve()}),g}}}); \ No newline at end of file diff --git a/navigation/amd/src/ajax_response_renderer.js b/navigation/amd/src/ajax_response_renderer.js new file mode 100644 index 0000000..99f2e28 --- /dev/null +++ b/navigation/amd/src/ajax_response_renderer.js @@ -0,0 +1,165 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Parse the response from the navblock ajax page and render the correct DOM + * structure for the tree from it. + * + * @module block_navigation/ajax_response_renderer + * @package core + * @copyright 2015 John Okely <john@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/templates', 'core/notification', 'core/url'], function($, Templates, Notification, Url) { + + // Mappings for the different types of nodes coming from the navigation. + // Copied from lib/navigationlib.php navigation_node constants. + var NODETYPE = { + // @type int Activity (course module) = 40. + ACTIVITY: 40, + // @type int Resource (course module = 50. + RESOURCE: 50, + }; + + /** + * Build DOM. + * + * @method buildDOM + * @param {Object} rootElement the root element of DOM. + * @param {object} nodes jquery object representing the nodes to be build. + */ + function buildDOM(rootElement, nodes) { + var ul = $('<ul></ul>'); + ul.attr('role', 'group'); + ul.attr('aria-hidden', true); + + $.each(nodes, function(index, node) { + if (typeof node !== 'object') { + return; + } + + var li = $('<li></li>'); + var p = $('<p></p>'); + var id = node.id || node.key + '_tree_item'; + var icon = null; + var isBranch = (node.expandable || node.haschildren) ? true : false; + + p.addClass('tree_item'); + p.attr('id', id); + p.attr('role', 'treeitem'); + // Negative tab index to allow it to receive focus. + p.attr('tabindex', '-1'); + + if (node.requiresajaxloading) { + p.attr('data-requires-ajax', true); + p.attr('data-node-id', node.id); + p.attr('data-node-key', node.key); + p.attr('data-node-type', node.type); + } + + if (isBranch) { + li.addClass('collapsed contains_branch'); + p.attr('aria-expanded', false); + p.addClass('branch'); + } + + var eleToAddIcon = null; + if (node.link) { + var link = $('<a title="' + node.title + '" href="' + node.link + '"></a>'); + + eleToAddIcon = link; + link.append('<span class="item-content-wrap">' + node.name + '</span>'); + + if (node.hidden) { + link.addClass('dimmed'); + } + + p.append(link); + } else { + var span = $('<span></span>'); + + eleToAddIcon = span; + span.append('<span class="item-content-wrap">' + node.name + '</span>'); + + if (node.hidden) { + span.addClass('dimmed'); + } + + p.append(span); + } + + if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) { + li.addClass('item_with_icon'); + p.addClass('hasicon'); + + if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) { + icon = $('<img/>'); + icon.attr('alt', node.icon.alt); + icon.attr('title', node.icon.title); + icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component)); + $.each(node.icon.classes, function(index, className) { + icon.addClass(className); + }); + eleToAddIcon.prepend(icon); + } else { + if (node.icon.component == 'moodle') { + node.icon.component = 'core'; + } + Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) { + // Prepend. + eleToAddIcon.prepend(html); + return; + }).catch(Notification.exception); + } + } + + li.append(p); + ul.append(li); + + if (node.children && node.children.length) { + buildDOM(p, node.children); + } else if (isBranch && !node.requiresajaxloading) { + li.removeClass('contains_branch'); + p.addClass('emptybranch'); + } + }); + + rootElement.parent().append(ul); + var id = rootElement.attr('id') + '_group'; + ul.attr('id', id); + rootElement.attr('aria-owns', id); + rootElement.attr('role', 'treeitem'); + } + + return { + render: function(element, nodes) { + // The first element of the response is the existing node so we start with processing the children. + if (nodes.children && nodes.children.length) { + buildDOM(element, nodes.children); + + var item = element.children("[role='treeitem']").first(); + var group = element.find('#' + item.attr('aria-owns')); + + item.attr('aria-expanded', true); + group.attr('aria-hidden', false); + } else { + if (element.parent().hasClass('contains_branch')) { + element.parent().removeClass('contains_branch'); + element.addClass('emptybranch'); + } + } + } + }; +}); diff --git a/navigation/amd/src/nav_loader.js b/navigation/amd/src/nav_loader.js new file mode 100644 index 0000000..ce5cb99 --- /dev/null +++ b/navigation/amd/src/nav_loader.js @@ -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/>. + +/** + * Load the nav tree items via ajax and render the response. + * + * @module block_navigation/nav_loader + * @package core + * @copyright 2015 John Okely <john@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'], + function($, ajax, config, renderer) { + var URL = config.wwwroot + '/lib/ajax/getnavbranch.php'; + + /** + * Get the block instance id. + * + * @function getBlockInstanceId + * @param {Element} element + * @returns {String} the instance id + */ + function getBlockInstanceId(element) { + return element.closest('[data-block]').attr('data-instanceid'); + } + + return { + load: function(element) { + element = $(element); + var promise = $.Deferred(); + var data = { + elementid: element.attr('data-node-id'), + id: element.attr('data-node-key'), + type: element.attr('data-node-type'), + sesskey: config.sesskey, + instance: getBlockInstanceId(element) + }; + var settings = { + type: 'POST', + dataType: 'json', + data: data + }; + + $.ajax(URL, settings).done(function(nodes) { + renderer.render(element, nodes); + promise.resolve(); + }); + + return promise; + } + }; +}); diff --git a/navigation/amd/src/navblock.js b/navigation/amd/src/navblock.js new file mode 100644 index 0000000..40e8678 --- /dev/null +++ b/navigation/amd/src/navblock.js @@ -0,0 +1,46 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Load the navigation tree javascript. + * + * @module block_navigation/navblock + * @package core + * @copyright 2015 John Okely <john@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/tree'], function($, Tree) { + return { + init: function(instanceid) { + var navTree = new Tree(".block_navigation .block_tree"); + navTree.finishExpandingGroup = function(item) { + Tree.prototype.finishExpandingGroup.call(this, item); + Y.use('moodle-core-event', function() { + Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, { + instanceid: instanceid + }); + }); + }; + navTree.collapseGroup = function(item) { + Tree.prototype.collapseGroup.call(this, item); + Y.use('moodle-core-event', function() { + Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, { + instanceid: instanceid + }); + }); + }; + } + }; +}); diff --git a/navigation/amd/src/site_admin_loader.js b/navigation/amd/src/site_admin_loader.js new file mode 100644 index 0000000..b203aac --- /dev/null +++ b/navigation/amd/src/site_admin_loader.js @@ -0,0 +1,52 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Load the site admin nav tree via ajax and render the response. + * + * @module block_navigation/site_admin_loader + * @package core + * @copyright 2015 John Okely <john@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'], + function($, ajax, config, renderer) { + + var SITE_ADMIN_NODE_TYPE = 71; + var URL = config.wwwroot + '/lib/ajax/getsiteadminbranch.php'; + + return { + load: function(element) { + element = $(element); + var promise = $.Deferred(); + var data = { + type: SITE_ADMIN_NODE_TYPE, + sesskey: config.sesskey + }; + var settings = { + type: 'POST', + dataType: 'json', + data: data + }; + + $.ajax(URL, settings).done(function(nodes) { + renderer.render(element, nodes); + promise.resolve(); + }); + + return promise; + } + }; +}); diff --git a/navigation/block_navigation.php b/navigation/block_navigation.php new file mode 100644 index 0000000..e7a5c11 --- /dev/null +++ b/navigation/block_navigation.php @@ -0,0 +1,336 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 classes used to manage the navigation structures in Moodle + * and was introduced as part of the changes occuring in Moodle 2.0 + * + * @since Moodle 2.0 + * @package block_navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The global navigation tree block class + * + * Used to produce the global navigation block new to Moodle 2.0 + * + * @package block_navigation + * @category navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_navigation extends block_base { + + /** @var int This allows for multiple navigation trees */ + public static $navcount; + /** @var string The name of the block */ + public $blockname = null; + /** @var bool A switch to indicate whether content has been generated or not. */ + protected $contentgenerated = false; + /** @var bool|null variable for checking if the block is docked*/ + protected $docked = null; + + /** @var int Trim characters from the right */ + const TRIM_RIGHT = 1; + /** @var int Trim characters from the left */ + const TRIM_LEFT = 2; + /** @var int Trim characters from the center */ + const TRIM_CENTER = 3; + + /** + * Set the initial properties for the block + */ + function init() { + $this->blockname = get_class($this); + $this->title = get_string('pluginname', $this->blockname); + } + + /** + * All multiple instances of this block + * @return bool Returns false + */ + function instance_allow_multiple() { + return false; + } + + /** + * Set the applicable formats for this block to all + * @return array + */ + function applicable_formats() { + return array('all' => true); + } + + /** + * Allow the user to configure a block instance + * @return bool Returns true + */ + function instance_allow_config() { + return true; + } + + /** + * The navigation block cannot be hidden by default as it is integral to + * the navigation of Moodle. + * + * @return false + */ + function instance_can_be_hidden() { + return false; + } + + /** + * Find out if an instance can be docked. + * + * @return bool true or false depending on whether the instance can be docked or not. + */ + function instance_can_be_docked() { + return (parent::instance_can_be_docked() && (empty($this->config->enabledock) || $this->config->enabledock=='yes')); + } + + /** + * Gets Javascript that may be required for navigation + */ + function get_required_javascript() { + parent::get_required_javascript(); + $arguments = array( + 'instanceid' => $this->instance->id + ); + $this->page->requires->string_for_js('viewallcourses', 'moodle'); + $this->page->requires->js_call_amd('block_navigation/navblock', 'init', $arguments); + } + + /** + * Gets the content for this block by grabbing it from $this->page + * + * @return object $this->content + */ + function get_content() { + global $CFG; + // First check if we have already generated, don't waste cycles + if ($this->contentgenerated === true) { + return $this->content; + } + // JS for navigation moved to the standard theme, the code will probably have to depend on the actual page structure + // $this->page->requires->js('/lib/javascript-navigation.js'); + // Navcount is used to allow us to have multiple trees although I dont' know why + // you would want two trees the same + + block_navigation::$navcount++; + + // Check if this block has been docked + if ($this->docked === null) { + $this->docked = get_user_preferences('nav_in_tab_panel_globalnav'.block_navigation::$navcount, 0); + } + + // Check if there is a param to change the docked state + if ($this->docked && optional_param('undock', null, PARAM_INT)==$this->instance->id) { + unset_user_preference('nav_in_tab_panel_globalnav'.block_navigation::$navcount); + $url = $this->page->url; + $url->remove_params(array('undock')); + redirect($url); + } else if (!$this->docked && optional_param('dock', null, PARAM_INT)==$this->instance->id) { + set_user_preferences(array('nav_in_tab_panel_globalnav'.block_navigation::$navcount=>1)); + $url = $this->page->url; + $url->remove_params(array('dock')); + redirect($url); + } + + $trimmode = self::TRIM_RIGHT; + $trimlength = 50; + + if (!empty($this->config->trimmode)) { + $trimmode = (int)$this->config->trimmode; + } + + if (!empty($this->config->trimlength)) { + $trimlength = (int)$this->config->trimlength; + } + + // Get the navigation object or don't display the block if none provided. + if (!$navigation = $this->get_navigation()) { + return null; + } + $expansionlimit = null; + if (!empty($this->config->expansionlimit)) { + $expansionlimit = $this->config->expansionlimit; + $navigation->set_expansion_limit($this->config->expansionlimit); + } + $this->trim($navigation, $trimmode, $trimlength, ceil($trimlength/2)); + + // Get the expandable items so we can pass them to JS + $expandable = array(); + $navigation->find_expandable($expandable); + if ($expansionlimit) { + foreach ($expandable as $key=>$node) { + if ($node['type'] > $expansionlimit && !($expansionlimit == navigation_node::TYPE_COURSE && $node['type'] == $expansionlimit && $node['branchid'] == SITEID)) { + unset($expandable[$key]); + } + } + } + + $limit = 20; + if (!empty($CFG->navcourselimit)) { + $limit = $CFG->navcourselimit; + } + $expansionlimit = 0; + if (!empty($this->config->expansionlimit)) { + $expansionlimit = $this->config->expansionlimit; + } + $arguments = array( + 'id' => $this->instance->id, + 'instance' => $this->instance->id, + 'candock' => $this->instance_can_be_docked(), + 'courselimit' => $limit, + 'expansionlimit' => $expansionlimit + ); + + $options = array(); + $options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes'); + + // Grab the items to display + $renderer = $this->page->get_renderer($this->blockname); + $this->content = new stdClass(); + $this->content->text = $renderer->navigation_tree($navigation, $expansionlimit, $options); + + // Set content generated to true so that we know it has been done + $this->contentgenerated = true; + + return $this->content; + } + + /** + * Returns the navigation + * + * @return navigation_node The navigation object to display + */ + protected function get_navigation() { + // Initialise (only actually happens if it hasn't already been done yet) + $this->page->navigation->initialise(); + return clone($this->page->navigation); + } + + /** + * Returns the attributes to set for this block + * + * This function returns an array of HTML attributes for this block including + * the defaults. + * {@link block_tree::html_attributes()} is used to get the default arguments + * and then we check whether the user has enabled hover expansion and add the + * appropriate hover class if it has. + * + * @return array An array of HTML attributes + */ + public function html_attributes() { + $attributes = parent::html_attributes(); + if (!empty($this->config->enablehoverexpansion) && $this->config->enablehoverexpansion == 'yes') { + $attributes['class'] .= ' block_js_expansion'; + } + return $attributes; + } + + /** + * Trims the text and shorttext properties of this node and optionally + * all of its children. + * + * @param navigation_node $node + * @param int $mode One of navigation_node::TRIM_* + * @param int $long The length to trim text to + * @param int $short The length to trim shorttext to + * @param bool $recurse Recurse all children + */ + public function trim(navigation_node $node, $mode=1, $long=50, $short=25, $recurse=true) { + switch ($mode) { + case self::TRIM_RIGHT : + if (core_text::strlen($node->text)>($long+3)) { + // Truncate the text to $long characters + $node->text = $this->trim_right($node->text, $long); + } + if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { + // Truncate the shorttext + $node->shorttext = $this->trim_right($node->shorttext, $short); + } + break; + case self::TRIM_LEFT : + if (core_text::strlen($node->text)>($long+3)) { + // Truncate the text to $long characters + $node->text = $this->trim_left($node->text, $long); + } + if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { + // Truncate the shorttext + $node->shorttext = $this->trim_left($node->shorttext, $short); + } + break; + case self::TRIM_CENTER : + if (core_text::strlen($node->text)>($long+3)) { + // Truncate the text to $long characters + $node->text = $this->trim_center($node->text, $long); + } + if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { + // Truncate the shorttext + $node->shorttext = $this->trim_center($node->shorttext, $short); + } + break; + } + if ($recurse && $node->children->count()) { + foreach ($node->children as &$child) { + $this->trim($child, $mode, $long, $short, true); + } + } + } + /** + * Truncate a string from the left + * @param string $string The string to truncate + * @param int $length The length to truncate to + * @return string The truncated string + */ + protected function trim_left($string, $length) { + return '...'.core_text::substr($string, core_text::strlen($string)-$length, $length); + } + /** + * Truncate a string from the right + * @param string $string The string to truncate + * @param int $length The length to truncate to + * @return string The truncated string + */ + protected function trim_right($string, $length) { + return core_text::substr($string, 0, $length).'...'; + } + /** + * Truncate a string in the center + * @param string $string The string to truncate + * @param int $length The length to truncate to + * @return string The truncated string + */ + protected function trim_center($string, $length) { + $trimlength = ceil($length/2); + $start = core_text::substr($string, 0, $trimlength); + $end = core_text::substr($string, core_text::strlen($string)-$trimlength); + $string = $start.'...'.$end; + return $string; + } + + /** + * Returns the role that best describes the navigation block... 'navigation' + * + * @return string 'navigation' + */ + public function get_aria_role() { + return 'navigation'; + } +} diff --git a/navigation/classes/privacy/provider.php b/navigation/classes/privacy/provider.php new file mode 100644 index 0000000..032a554 --- /dev/null +++ b/navigation/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_navigation. + * + * @package block_navigation + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_navigation\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_navigation implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/navigation/db/access.php b/navigation/db/access.php new file mode 100644 index 0000000..6754273 --- /dev/null +++ b/navigation/db/access.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/>. + +/** + * Navigation block caps. + * + * @package block_navigation + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/navigation:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/navigation: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/navigation/db/upgrade.php b/navigation/db/upgrade.php new file mode 100644 index 0000000..07b4381 --- /dev/null +++ b/navigation/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the navigation block + * + * 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. + * + * @since Moodle 2.0 + * @package block_navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * As of the implementation of this block and the general navigation code + * in Moodle 2.0 the body of immediate upgrade work for this block and + * settings is done in core upgrade {@see lib/db/upgrade.php} + * + * There were several reasons that they were put there and not here, both becuase + * the process for the two blocks was very similar and because the upgrade process + * was complex due to us wanting to remvoe the outmoded blocks that this + * block was going to replace. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_navigation_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/navigation/edit_form.php b/navigation/edit_form.php new file mode 100644 index 0000000..9db482a --- /dev/null +++ b/navigation/edit_form.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/>. + +/** + * Form for editing global navigation instances. + * + * @since Moodle 2.0 + * @package block_navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing global navigation instances. + * + * @package block_navigation + * @category navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_navigation_edit_form extends block_edit_form { + /** + * @param MoodleQuickForm $mform + */ + protected function specific_definition($mform) { + global $CFG; + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mods = array('enabledock'=>'yes', 'linkcategories'=>'no'); + $yesnooptions = array('yes'=>get_string('yes'), 'no'=>get_string('no')); + foreach ($mods as $modname=>$default) { + $mform->addElement('select', 'config_'.$modname, get_string($modname.'desc', $this->block->blockname), $yesnooptions); + $mform->setDefault('config_'.$modname, $default); + } + + $options = array( + block_navigation::TRIM_RIGHT => get_string('trimmoderight', $this->block->blockname), + block_navigation::TRIM_LEFT => get_string('trimmodeleft', $this->block->blockname), + block_navigation::TRIM_CENTER => get_string('trimmodecenter', $this->block->blockname) + ); + $mform->addElement('select', 'config_trimmode', get_string('trimmode', $this->block->blockname), $options); + $mform->setType('config_trimmode', PARAM_INT); + + $mform->addElement('text', 'config_trimlength', get_string('trimlength', $this->block->blockname)); + $mform->setDefault('config_trimlength', 50); + $mform->setType('config_trimlength', PARAM_INT); + + $options = array( + 0 => get_string('everything', $this->block->blockname), + global_navigation::TYPE_COURSE => get_string('courses', $this->block->blockname), + global_navigation::TYPE_SECTION => get_string('coursestructures', $this->block->blockname), + global_navigation::TYPE_ACTIVITY => get_string('courseactivities', $this->block->blockname) + ); + $mform->addElement('select', 'config_expansionlimit', get_string('expansionlimit', $this->block->blockname), $options); + $mform->setType('config_expansionlimit', PARAM_INT); + + } +} \ No newline at end of file diff --git a/navigation/lang/en/block_navigation.php b/navigation/lang/en/block_navigation.php new file mode 100644 index 0000000..89c95e6 --- /dev/null +++ b/navigation/lang/en/block_navigation.php @@ -0,0 +1,42 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 language strings used in the global navigation block + * + * @since Moodle 2.0 + * @package block_navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['everything'] = 'Everything'; +$string['courses'] = 'Categories and courses'; +$string['coursestructures'] = 'Categories, courses, and course structures'; +$string['courseactivities'] = 'Categories, courses, and course Activities'; +$string['enabledockdesc'] = 'Allow the user to dock this block'; +$string['expansionlimit'] = 'Generate navigation for the following'; +$string['linkcategoriesdesc'] = 'Display categories as links'; +$string['navigation:addinstance'] = 'Add a new navigation block'; +$string['navigation:myaddinstance'] = 'Add a new navigation block to Dashboard'; +$string['pluginname'] = 'Navigation'; +$string['trimmode'] = 'Trim mode'; +$string['trimmoderight'] = 'Trim characters from the right'; +$string['trimmodeleft'] = 'Trim characters from the left'; +$string['trimmodecenter'] = 'Trim characters from the center'; +$string['trimlength'] = 'How many characters to trim to'; +$string['privacy:metadata'] = 'The Navigation block only shows data stored in other locations.'; diff --git a/navigation/renderer.php b/navigation/renderer.php new file mode 100644 index 0000000..c64e55d --- /dev/null +++ b/navigation/renderer.php @@ -0,0 +1,192 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Outputs the navigation tree. + * + * @since Moodle 2.0 + * @package block_navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Renderer for block navigation + * + * @package block_navigation + * @category navigation + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_navigation_renderer extends plugin_renderer_base { + + /** + * Returns the content of the navigation tree. + * + * @param global_navigation $navigation + * @param int $expansionlimit + * @param array $options + * @return string $content + */ + public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) { + $navigation->add_class('navigation_node'); + $navigationattrs = array( + 'class' => 'block_tree list', + 'role' => 'tree', + 'data-ajax-loader' => 'block_navigation/nav_loader'); + $content = $this->navigation_node(array($navigation), $navigationattrs, $expansionlimit, $options); + if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) { + $content = $this->output->box($content, 'block_tree_box', $navigation->id); + } + return $content; + } + /** + * Produces a navigation node for the navigation tree + * + * @param navigation_node[] $items + * @param array $attrs + * @param int $expansionlimit + * @param array $options + * @param int $depth + * @return string + */ + protected function navigation_node($items, $attrs=array(), $expansionlimit=null, array $options = array(), $depth=1) { + // Exit if empty, we don't want an empty ul element. + if (count($items) === 0) { + return ''; + } + + // Turn our navigation items into list items. + $lis = array(); + // Set the number to be static for unique id's. + static $number = 0; + foreach ($items as $item) { + $number++; + if (!$item->display && !$item->contains_active_node()) { + continue; + } + + $isexpandable = (empty($expansionlimit) || ($item->type > navigation_node::TYPE_ACTIVITY || $item->type < $expansionlimit) || ($item->contains_active_node() && $item->children->count() > 0)); + + // Skip elements which have no content and no action - no point in showing them + if (!$isexpandable && empty($item->action)) { + continue; + } + + $id = $item->id ? $item->id : html_writer::random_id(); + $content = $item->get_content(); + $title = $item->get_title(); + $ulattr = ['id' => $id . '_group', 'role' => 'group']; + $liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth]]; + $pattr = ['class' => ['tree_item'], 'role' => 'treeitem']; + $pattr += !empty($item->id) ? ['id' => $item->id] : []; + $isbranch = $isexpandable && ($item->children->count() > 0 || ($item->has_children() && (isloggedin() || $item->type <= navigation_node::TYPE_CATEGORY))); + $hasicon = ((!$isbranch || $item->type == navigation_node::TYPE_ACTIVITY || $item->type == navigation_node::TYPE_RESOURCE) && $item->icon instanceof renderable); + $icon = ''; + + if ($hasicon) { + $liattr['class'][] = 'item_with_icon'; + $pattr['class'][] = 'hasicon'; + $icon = $this->output->render($item->icon); + // Because an icon is being used we're going to wrap the actual content in a span. + // This will allow designers to create columns for the content, as we've done in styles.css. + $content = $icon . html_writer::span($content, 'item-content-wrap'); + } + if ($item->helpbutton !== null) { + $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton')); + } + if (empty($content)) { + continue; + } + + $nodetextid = 'label_' . $depth . '_' . $number; + $attributes = array('tabindex' => '-1', 'id' => $nodetextid); + if ($title !== '') { + $attributes['title'] = $title; + } + if ($item->hidden) { + $attributes['class'] = 'dimmed_text'; + } + if (is_string($item->action) || empty($item->action) || + (($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) && + empty($options['linkcategories']))) { + $content = html_writer::tag('span', $content, $attributes); + } else if ($item->action instanceof action_link) { + //TODO: to be replaced with something else + $link = $item->action; + $link->text = $icon.html_writer::span($link->text, 'item-content-wrap'); + $link->attributes = array_merge($link->attributes, $attributes); + $content = $this->output->render($link); + } else if ($item->action instanceof moodle_url) { + $content = html_writer::link($item->action, $content, $attributes); + } + + if ($isbranch) { + $pattr['class'][] = 'branch'; + $liattr['class'][] = 'contains_branch'; + $pattr += ['aria-expanded' => ($item->has_children() && (!$item->forceopen || $item->collapse)) ? "false" : "true"]; + if ($item->requiresajaxloading) { + $pattr += [ + 'data-requires-ajax' => 'true', + 'data-loaded' => 'false', + 'data-node-id' => $item->id, + 'data-node-key' => $item->key, + 'data-node-type' => $item->type + ]; + } else { + $pattr += ['aria-owns' => $id . '_group']; + } + } + + if ($item->isactive === true) { + $liattr['class'][] = 'current_branch'; + } + if (!empty($item->classes) && count($item->classes)>0) { + $pattr['class'] = array_merge($pattr['class'], $item->classes); + } + + $liattr['class'] = join(' ', $liattr['class']); + $pattr['class'] = join(' ', $pattr['class']); + + $pattr += $depth == 1 ? ['data-collapsible' => 'false'] : []; + if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') { + $ulattr += ['aria-hidden' => 'true']; + } + + // Create the structure. + $content = html_writer::tag('p', $content, $pattr); + if ($isexpandable) { + $content .= $this->navigation_node($item->children, $ulattr, $expansionlimit, $options, $depth + 1); + } + if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) { + $content = html_writer::empty_tag('hr') . $content; + } + + $liattr['aria-labelledby'] = $nodetextid; + $content = html_writer::tag('li', $content, $liattr); + $lis[] = $content; + } + + if (count($lis) === 0) { + // There is still a chance, despite having items, that nothing had content and no list items were created. + return ''; + } + + // We used to separate using new lines, however we don't do that now, instead we'll save a few chars. + // The source is complex already anyway. + return html_writer::tag('ul', implode('', $lis), $attrs); + } +} diff --git a/navigation/styles.css b/navigation/styles.css new file mode 100644 index 0000000..2369c1a --- /dev/null +++ b/navigation/styles.css @@ -0,0 +1,76 @@ +.block_navigation .block_tree .depth_1 > .tree_item.branch { + padding-left: 0; + background-image: none; +} + +.block_navigation .block_tree .depth_1 > ul { + margin: 0; +} + +.block_navigation .block_tree ul { + margin-left: 18px; +} + +.block_navigation .block_tree p.hasicon { + text-indent: -21px; + padding-left: 21px; +} + +.block_navigation .block_tree p.hasicon img { + width: 16px; + height: 16px; + margin-top: 3px; + margin-right: 5px; + vertical-align: top; +} + +.block_navigation .block_tree p.hasicon.visibleifjs { + display: block; +} + +.block_navigation .block_tree .tree_item { + cursor: pointer; + padding-left: 0; + margin: 3px 0; + background-position: 0 50%; + background-repeat: no-repeat; + word-wrap: break-word; +} + +.block_navigation .block_tree .tree_item.branch { + padding-left: 21px; +} + +.block_navigation .block_tree .active_tree_node { + font-weight: bold; +} + +.block_navigation .block_tree [aria-expanded="true"] { + background-image: url('[[pix:t/expanded]]'); +} + +.block_navigation .block_tree [aria-expanded="false"] { + background-image: url('[[pix:t/collapsed]]'); +} + +.block_navigation .block_tree [aria-expanded="true"].emptybranch { + background-image: url('[[pix:t/collapsed_empty]]'); +} + +.block_navigation .block_tree [aria-expanded="false"].loading { + background-image: url('[[pix:i/loading_small]]'); +} + +/*rtl:raw: +.block_navigation .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed_rtl]]');} +.block_navigation .block_tree [aria-expanded="true"].emptybranch {background-image: url('[[pix:t/collapsed_empty_rtl]]');} +.block_navigation .block_tree [aria-expanded="false"].loading {background-image: url('[[pix:i/loading_small]]');} +*/ + +.block_navigation .block_tree [aria-hidden="false"] { + display: block; +} + +.block_navigation .block_tree [aria-hidden="true"]:not(.icon) { + display: none; +} diff --git a/navigation/tests/behat/expand_courses_node.feature b/navigation/tests/behat/expand_courses_node.feature new file mode 100644 index 0000000..683c34a --- /dev/null +++ b/navigation/tests/behat/expand_courses_node.feature @@ -0,0 +1,202 @@ +@block @block_navigation +Feature: Expand the courses nodes within the navigation block + In order to navigate the site + As an anonymous user, a guest, a student, and an admin + I need to expand the courses node in the navigation block and check the display of courses and categories. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "categories" exist: + | name | category | idnumber | visible | + | cat1 | 0 | cat1 | 1 | + | cat2 | 0 | cat2 | 1 | + | cat21 | cat2 | cat21 | 1 | + | cat211 | cat21 | cat211 | 1 | + | cat3 | 0 | cat3 | 0 | + And the following "courses" exist: + | fullname | shortname | category | visible | + | Course 1 | c1 | cat1 | 1 | + | Course 2 | c2 | cat2 | 1 | + | Course 3 | c3 | cat21 | 1 | + | Course 4 | c4 | cat211 | 1 | + | Course 5 | c5 | cat211 | 0 | + | Course 6 | c6 | cat211 | 0 | + | Course 7 | c7 | cat3 | 1 | + | Course 8 | c8 | cat3 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | c1 | teacher | + | teacher1 | c3 | teacher | + | teacher1 | c5 | teacher | + | student1 | c1 | student | + | student1 | c2 | student | + | student1 | c4 | student | + And the following config values are set as admin: + | navshowallcourses | 1 | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I configure the "Navigation" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + And I turn editing mode off + And I am on "Course 2" course homepage + And I navigate to "Enrolment methods" node in "Course administration > Users" + And I click on "Edit" "link" in the "Guest access" "table_row" + And I set the following fields to these values: + | Allow guest access | Yes | + And I press "Save changes" + And I log out + + @javascript + Scenario: As an anonymous user I expand the courses node to see courses. + When I should see "You are not logged in." in the ".logininfo" "css_element" + And I should see "Home" in the "Navigation" "block" + And I should see "Courses" in the "Navigation" "block" + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should not see "cat3" in the "Navigation" "block" + And I expand "cat1" node + And I expand "cat2" node + And I should see "cat21" in the "Navigation" "block" + And I expand "cat21" node + And I should see "cat211" in the "Navigation" "block" + And I expand "cat211" node + Then I should see "c1" in the "Navigation" "block" + And I should see "c2" in the "Navigation" "block" + And I should see "c3" in the "Navigation" "block" + And I should see "c4" in the "Navigation" "block" + And I should not see "c5" in the "Navigation" "block" + And I should not see "c6" in the "Navigation" "block" + And navigation node "c1" should not be expandable + And navigation node "c2" should not be expandable + And navigation node "c3" should not be expandable + And navigation node "c4" should not be expandable + + @javascript + Scenario: As the admin user I expand the courses and category nodes to see courses. + When I log in as "admin" + And I am on site homepage + And I should see "Site home" in the "Navigation" "block" + And I should see "Courses" in the "Navigation" "block" + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should see "cat3" in the "Navigation" "block" + And I expand "cat1" node + And I expand "cat2" node + And I expand "cat3" node + And I should see "cat21" in the "Navigation" "block" + And I expand "cat21" node + And I should see "cat211" in the "Navigation" "block" + And I expand "cat211" node + Then I should see "c1" in the "Navigation" "block" + And I should see "c2" in the "Navigation" "block" + And I should see "c3" in the "Navigation" "block" + And I should see "c4" in the "Navigation" "block" + And I should see "c5" in the "Navigation" "block" + And I should see "c6" in the "Navigation" "block" + And I should see "c7" in the "Navigation" "block" + And I should see "c8" in the "Navigation" "block" + And navigation node "c1" should be expandable + And navigation node "c2" should be expandable + And navigation node "c3" should be expandable + And navigation node "c4" should be expandable + And navigation node "c5" should be expandable + And navigation node "c6" should be expandable + And navigation node "c7" should be expandable + And navigation node "c8" should be expandable + + @javascript + Scenario: As teacher1 I expand the courses and category nodes to see courses. + When I log in as "teacher1" + And I am on site homepage + And I should see "Site home" in the "Navigation" "block" + And I should see "Courses" in the "Navigation" "block" + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should not see "cat3" in the "Navigation" "block" + And I expand "cat1" node + And I expand "cat2" node + And I should see "cat21" in the "Navigation" "block" + And I expand "cat21" node + And I should see "cat211" in the "Navigation" "block" + And I expand "cat211" node + Then I should see "c1" in the "Navigation" "block" + And I should see "c2" in the "Navigation" "block" + And I should see "c3" in the "Navigation" "block" + And I should see "c4" in the "Navigation" "block" + And I should see "c5" in the "Navigation" "block" + And I should not see "c6" in the "Navigation" "block" + And I should not see "c7" in the "Navigation" "block" + And I should not see "c8" in the "Navigation" "block" + And navigation node "c1" should be expandable + And navigation node "c2" should be expandable + And navigation node "c3" should be expandable + And navigation node "c4" should not be expandable + And navigation node "c5" should be expandable + + @javascript + Scenario: As student1 I expand the courses and category nodes to see courses. + When I log in as "student1" + And I am on site homepage + And I should see "Site home" in the "Navigation" "block" + And I should see "Courses" in the "Navigation" "block" + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should not see "cat3" in the "Navigation" "block" + And I expand "cat1" node + And I expand "cat2" node + And I should see "cat21" in the "Navigation" "block" + And I expand "cat21" node + And I should see "cat211" in the "Navigation" "block" + And I expand "cat211" node + Then I should see "c1" in the "Navigation" "block" + And I should see "c2" in the "Navigation" "block" + And I should see "c3" in the "Navigation" "block" + And I should see "c4" in the "Navigation" "block" + And I should not see "c5" in the "Navigation" "block" + And I should not see "c6" in the "Navigation" "block" + And I should not see "c7" in the "Navigation" "block" + And I should not see "c8" in the "Navigation" "block" + And navigation node "c1" should be expandable + And navigation node "c2" should be expandable + And navigation node "c3" should not be expandable + And navigation node "c4" should be expandable + + @javascript + Scenario: As guest I expand the courses and category nodes to see courses. + When I log in as "guest" + And I am on site homepage + And I should see "Home" in the "Navigation" "block" + And I should see "Courses" in the "Navigation" "block" + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should not see "cat3" in the "Navigation" "block" + And I expand "cat1" node + And I expand "cat2" node + And I should see "cat21" in the "Navigation" "block" + And I expand "cat21" node + And I should see "cat211" in the "Navigation" "block" + And I expand "cat211" node + Then I should see "c1" in the "Navigation" "block" + And I should see "c2" in the "Navigation" "block" + And I should see "c3" in the "Navigation" "block" + And I should see "c4" in the "Navigation" "block" + And I should not see "c5" in the "Navigation" "block" + And I should not see "c6" in the "Navigation" "block" + And I should not see "c7" in the "Navigation" "block" + And I should not see "c8" in the "Navigation" "block" + And navigation node "c1" should not be expandable + And navigation node "c2" should be expandable + And navigation node "c3" should not be expandable + And navigation node "c4" should not be expandable diff --git a/navigation/tests/behat/participants_link.feature b/navigation/tests/behat/participants_link.feature new file mode 100644 index 0000000..76533c9 --- /dev/null +++ b/navigation/tests/behat/participants_link.feature @@ -0,0 +1,54 @@ +@block @block_navigation +Feature: Displaying the link to the Participants page + In order to see the course / site participants + As a student / admin respectively + I need a link to the Participants page be displayed (but only if I can access that page) + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I configure the "Navigation" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + And I log out + + @javascript + Scenario: Course participants link is displayed to enrolled students after expanding the course node + When I log in as "student1" + And I expand "C1" node + Then "Participants" "link" should exist in the "Navigation" "block" + And I click on "Participants" "link" in the "Navigation" "block" + And I should see "Participants" + And "Student One" "link" should exist + And "Student Two" "link" should not exist + + Scenario: Site participants link is displayed to admins + When I log in as "admin" + Then "Participants" "link" should exist in the "Navigation" "block" + And I click on "Participants" "link" in the "Navigation" "block" + And I should see "Participants" + And "Student One" "link" should exist + And "Student Two" "link" should exist + + @javascript + Scenario: Site participants link is not displayed to students (MDL-55667) + Given I log in as "admin" + And I set the following administration settings values: + | defaultfrontpageroleid | Student (student) | + And I log out + When I log in as "student2" + And I expand "Site pages" node + Then "Participants" "link" should not exist in the "Navigation" "block" diff --git a/navigation/tests/behat/view_my_courses.feature b/navigation/tests/behat/view_my_courses.feature new file mode 100644 index 0000000..2b086ce --- /dev/null +++ b/navigation/tests/behat/view_my_courses.feature @@ -0,0 +1,104 @@ +@block @block_navigation +Feature: View my courses in navigation block + In order to navigate to my courses + As a student + I need my courses displayed in the navigation block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + And the following "categories" exist: + | name | category | idnumber | + | cat1 | 0 | cat1 | + | cat2 | 0 | cat2 | + | cat3 | 0 | cat3 | + | cat31 | cat3 | cat31 | + | cat32 | cat3 | cat32 | + | cat33 | cat3 | cat33 | + And the following "courses" exist: + | fullname | shortname | category | + | Course1 | c1 | cat1 | + | Course2 | c2 | cat2 | + | Course31 | c31 | cat31 | + | Course32 | c32 | cat32 | + | Course331| c331 | cat33 | + | Course332| c332 | cat33 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | c1 | student | + | student1 | c31 | student | + | student1 | c331 | student | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I configure the "Navigation" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + And I log out + + @javascript + Scenario: The plain list of enrolled courses is shown + Given the following config values are set as admin: + | navshowmycoursecategories | 0 | + And I log in as "student1" + When I click on "Dashboard" "link" in the "Navigation" "block" + Then I should not see "cat1" in the "Navigation" "block" + And I should not see "cat2" in the "Navigation" "block" + And I should see "c1" in the "Navigation" "block" + And I should see "c31" in the "Navigation" "block" + And I should see "c331" in the "Navigation" "block" + And I should not see "c2" in the "Navigation" "block" + And I should not see "c32" in the "Navigation" "block" + And I should not see "c332" in the "Navigation" "block" + + @javascript + Scenario: The nested list of enrolled courses is shown + Given the following config values are set as admin: + | navshowmycoursecategories | 1 | + And I log in as "student1" + When I click on "Dashboard" "link" in the "Navigation" "block" + Then I should see "cat1" in the "Navigation" "block" + And I should see "cat3" in the "Navigation" "block" + And I should not see "cat2" in the "Navigation" "block" + And I expand "cat3" node + And I should see "cat31" in the "Navigation" "block" + And I should see "cat33" in the "Navigation" "block" + And I should not see "cat32" in the "Navigation" "block" + And I expand "cat31" node + And I should see "c31" in the "Navigation" "block" + And I expand "cat33" node + And I should see "c331" in the "Navigation" "block" + And I should not see "c332" in the "Navigation" "block" + + @javascript + Scenario: I can expand categories and courses as guest + Given the following config values are set as admin: + | navshowmycoursecategories | 1 | + | navshowallcourses | 1 | + And I expand "Courses" node + And I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should see "cat3" in the "Navigation" "block" + And I should not see "cat31" in the "Navigation" "block" + And I should not see "cat32" in the "Navigation" "block" + And I should not see "cat331" in the "Navigation" "block" + And I should not see "c1" in the "Navigation" "block" + And I should not see "c2" in the "Navigation" "block" + And I should not see "c31" in the "Navigation" "block" + And I should not see "c32" in the "Navigation" "block" + When I expand "cat3" node + And I expand "cat31" node + And I expand "cat1" node + Then I should see "cat1" in the "Navigation" "block" + And I should see "cat2" in the "Navigation" "block" + And I should see "cat3" in the "Navigation" "block" + And I should see "cat31" in the "Navigation" "block" + And I should see "cat32" in the "Navigation" "block" + And I should not see "cat331" in the "Navigation" "block" + And I should see "c1" in the "Navigation" "block" + And I should not see "c2" in the "Navigation" "block" + And I should see "c31" in the "Navigation" "block" + And I should not see "c32" in the "Navigation" "block" diff --git a/navigation/version.php b/navigation/version.php new file mode 100644 index 0000000..2b08391 --- /dev/null +++ b/navigation/version.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/>. + +/** + * Version details + * + * @package block_navigation + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_navigation'; // Full name of the plugin (used for diagnostics) diff --git a/news_items/block_news_items.php b/news_items/block_news_items.php new file mode 100644 index 0000000..9cd9406 --- /dev/null +++ b/news_items/block_news_items.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/>. + +/** + * This file contains the news item block class, based upon block_base. + * + * @package block_news_items + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class block_news_items + * + * @package block_news_items + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_news_items extends block_base { + function init() { + $this->title = get_string('pluginname', 'block_news_items'); + } + + function get_content() { + global $CFG, $USER; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + + if ($this->page->course->newsitems) { // Create a nice listing of recent postings + + require_once($CFG->dirroot.'/mod/forum/lib.php'); // We'll need this + + $text = ''; + + if (!$forum = forum_get_course_forum($this->page->course->id, 'news')) { + return ''; + } + + $modinfo = get_fast_modinfo($this->page->course); + if (empty($modinfo->instances['forum'][$forum->id])) { + return ''; + } + $cm = $modinfo->instances['forum'][$forum->id]; + + if (!$cm->uservisible) { + return ''; + } + + $context = context_module::instance($cm->id); + + /// User must have perms to view discussions in that forum + if (!has_capability('mod/forum:viewdiscussion', $context)) { + return ''; + } + + /// First work out whether we can post to this group and if so, include a link + $groupmode = groups_get_activity_groupmode($cm); + $currentgroup = groups_get_activity_group($cm, true); + + if (forum_user_can_post_discussion($forum, $currentgroup, $groupmode, $cm, $context)) { + $text .= '<div class="newlink"><a href="'.$CFG->wwwroot.'/mod/forum/post.php?forum='.$forum->id.'">'. + get_string('addanewtopic', 'forum').'</a>...</div>'; + } + + /// Get all the recent discussions we're allowed to see + + // This block displays the most recent posts in a forum in + // descending order. The call to default sort order here will use + // that unless the discussion that post is in has a timestart set + // in the future. + // This sort will ignore pinned posts as we want the most recent. + $sort = forum_get_default_sort_order(true, 'p.modified', 'd', false); + if (! $discussions = forum_get_discussions($cm, $sort, false, + -1, $this->page->course->newsitems, + false, -1, 0, FORUM_POSTS_ALL_USER_GROUPS) ) { + $text .= '('.get_string('nonews', 'forum').')'; + $this->content->text = $text; + return $this->content; + } + + /// Actually create the listing now + + $strftimerecent = get_string('strftimerecent'); + $strmore = get_string('more', 'forum'); + + /// Accessibility: markup as a list. + $text .= "\n<ul class='unlist'>\n"; + foreach ($discussions as $discussion) { + + $discussion->subject = $discussion->name; + + $discussion->subject = format_string($discussion->subject, true, $forum->course); + + $text .= '<li class="post">'. + '<div class="head clearfix">'. + '<div class="date">'.userdate($discussion->modified, $strftimerecent).'</div>'. + '<div class="name">'.fullname($discussion).'</div></div>'. + '<div class="info"><a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->discussion.'">'.$discussion->subject.'</a></div>'. + "</li>\n"; + } + $text .= "</ul>\n"; + + $this->content->text = $text; + + $this->content->footer = '<a href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'. + get_string('oldertopics', 'forum').'</a> ...'; + + /// If RSS is activated at site and forum level and this forum has rss defined, show link + if (isset($CFG->enablerssfeeds) && isset($CFG->forum_enablerssfeeds) && + $CFG->enablerssfeeds && $CFG->forum_enablerssfeeds && $forum->rsstype && $forum->rssarticles) { + require_once($CFG->dirroot.'/lib/rsslib.php'); // We'll need this + if ($forum->rsstype == 1) { + $tooltiptext = get_string('rsssubscriberssdiscussions','forum'); + } else { + $tooltiptext = get_string('rsssubscriberssposts','forum'); + } + if (!isloggedin()) { + $userid = $CFG->siteguest; + } else { + $userid = $USER->id; + } + + $this->content->footer .= '<br />'.rss_get_link($context->id, $userid, 'mod_forum', $forum->id, $tooltiptext); + } + + } + + return $this->content; + } +} + + diff --git a/news_items/classes/privacy/provider.php b/news_items/classes/privacy/provider.php new file mode 100644 index 0000000..d4b883b --- /dev/null +++ b/news_items/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_news_items. + * + * @package block_news_items + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_news_items\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_news_items implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/news_items/db/access.php b/news_items/db/access.php new file mode 100644 index 0000000..86bb1b7 --- /dev/null +++ b/news_items/db/access.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/>. + +/** + * News items block caps. + * + * @package block_news_items + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/news_items:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/news_items: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/news_items/lang/en/block_news_items.php b/news_items/lang/en/block_news_items.php new file mode 100644 index 0000000..8287d4f --- /dev/null +++ b/news_items/lang/en/block_news_items.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_news_items', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_news_items + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['news_items:addinstance'] = 'Add a new latest announcements block'; +$string['news_items:myaddinstance'] = 'Add a new latest announcements block to Dashboard'; +$string['pluginname'] = 'Latest announcements'; +$string['privacy:metadata'] = 'The Latest announcements block only shows data stored in the forum and does not store data itself.'; diff --git a/news_items/tests/behat/display_news.feature b/news_items/tests/behat/display_news.feature new file mode 100644 index 0000000..dfc75ee --- /dev/null +++ b/news_items/tests/behat/display_news.feature @@ -0,0 +1,47 @@ +@block @block_news_items +Feature: Latest announcements block displays the course latest news + In order to be aware of the course announcements + As a user + I need to see the latest announcements block in the main course page + + @javascript + Scenario: Latest course announcements are displayed and can be configured + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And I log in as "admin" + And I create a course with: + | Course full name | Course 1 | + | Course short name | C1 | + | Number of announcements | 5 | + And I enrol "Teacher 1" user as "Teacher" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Latest announcements" block + And I turn editing mode off + When I add a new topic to "Announcements" forum with: + | Subject | Discussion One | + | Message | Not important | + And I add a new topic to "Announcements" forum with: + | Subject | Discussion Two | + | Message | Not important | + And I add a new topic to "Announcements" forum with: + | Subject | Discussion Three | + | Message | Not important | + And I am on "Course 1" course homepage + Then I should see "Discussion One" in the "Latest announcements" "block" + And I should see "Discussion Two" in the "Latest announcements" "block" + And I should see "Discussion Three" in the "Latest announcements" "block" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Number of announcements | 2 | + And I press "Save and display" + And I should not see "Discussion One" in the "Latest announcements" "block" + And I should see "Discussion Two" in the "Latest announcements" "block" + And I should see "Discussion Three" in the "Latest announcements" "block" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Number of announcements | 0 | + And I press "Save and display" + And "Latest announcements" "block" should not exist diff --git a/news_items/version.php b/news_items/version.php new file mode 100644 index 0000000..6802862 --- /dev/null +++ b/news_items/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_news_items + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_news_items'; // Full name of the plugin (used for diagnostics) +$plugin->dependencies = array('mod_forum' => 2018050800); diff --git a/online_users/block_online_users.php b/online_users/block_online_users.php new file mode 100644 index 0000000..08521a3 --- /dev/null +++ b/online_users/block_online_users.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/>. + +/** + * Online users block. + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use block_online_users\fetcher; + +/** + * This block needs to be reworked. + * The new roles system does away with the concepts of rigid student and + * teacher roles. + */ +class block_online_users extends block_base { + function init() { + $this->title = get_string('pluginname','block_online_users'); + } + + function has_config() { + return true; + } + + function get_content() { + global $USER, $CFG, $DB, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + $timetoshowusers = 300; //Seconds default + if (isset($CFG->block_online_users_timetosee)) { + $timetoshowusers = $CFG->block_online_users_timetosee * 60; + } + $now = time(); + + //Calculate if we are in separate groups + $isseparategroups = ($this->page->course->groupmode == SEPARATEGROUPS + && $this->page->course->groupmodeforce + && !has_capability('moodle/site:accessallgroups', $this->page->context)); + + //Get the user current group + $currentgroup = $isseparategroups ? groups_get_course_group($this->page->course) : NULL; + + $sitelevel = $this->page->course->id == SITEID || $this->page->context->contextlevel < CONTEXT_COURSE; + + $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $this->page->context, + $sitelevel, $this->page->course->id); + + //Calculate minutes + $minutes = floor($timetoshowusers/60); + $periodminutes = get_string('periodnminutes', 'block_online_users', $minutes); + + // Count users. + $usercount = $onlineusers->count_users(); + if ($usercount === 0) { + $usercount = get_string('nouser', 'block_online_users'); + } else if ($usercount === 1) { + $usercount = get_string('numuser', 'block_online_users', $usercount); + } else { + $usercount = get_string('numusers', 'block_online_users', $usercount); + } + + $this->content->text = '<div class="info">'.$usercount.' ('.$periodminutes.')</div>'; + + // Verify if we can see the list of users, if not just print number of users + if (!has_capability('block/online_users:viewlist', $this->page->context)) { + return $this->content; + } + + $userlimit = 50; // We'll just take the most recent 50 maximum. + if ($users = $onlineusers->get_users($userlimit)) { + foreach ($users as $user) { + $users[$user->id]->fullname = fullname($user); + } + } else { + $users = array(); + } + + //Now, we have in users, the list of users to show + //Because they are online + if (!empty($users)) { + //Accessibility: Don't want 'Alt' text for the user picture; DO want it for the envelope/message link (existing lang string). + //Accessibility: Converted <div> to <ul>, inherit existing classes & styles. + $this->content->text .= "<ul class='list'>\n"; + if (isloggedin() && has_capability('moodle/site:sendmessage', $this->page->context) + && !empty($CFG->messaging) && !isguestuser()) { + $canshowicon = true; + } else { + $canshowicon = false; + } + foreach ($users as $user) { + $this->content->text .= '<li class="listentry">'; + $timeago = format_time($now - $user->lastaccess); //bruno to calculate correctly on frontpage + + if (isguestuser($user)) { + $this->content->text .= '<div class="user">'.$OUTPUT->user_picture($user, array('size'=>16, 'alttext'=>false)); + $this->content->text .= get_string('guestuser').'</div>'; + + } else { + $this->content->text .= '<div class="user">'; + $this->content->text .= '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&course='.$this->page->course->id.'" title="'.$timeago.'">'; + $this->content->text .= $OUTPUT->user_picture($user, array('size'=>16, 'alttext'=>false, 'link'=>false)) .$user->fullname.'</a></div>'; + } + if ($canshowicon and ($USER->id != $user->id) and !isguestuser($user)) { // Only when logged in and messaging active etc + $anchortagcontents = $OUTPUT->pix_icon('t/message', get_string('messageselectadd')); + $anchorurl = new moodle_url('/message/index.php', array('id' => $user->id)); + $anchortag = html_writer::link($anchorurl, $anchortagcontents, + array('title' => get_string('messageselectadd'))); + + $this->content->text .= '<div class="message">'.$anchortag.'</div>'; + } + $this->content->text .= "</li>\n"; + } + $this->content->text .= '</ul><div class="clearer"><!-- --></div>'; + } + + return $this->content; + } +} + + diff --git a/online_users/classes/fetcher.php b/online_users/classes/fetcher.php new file mode 100644 index 0000000..1c6d15a --- /dev/null +++ b/online_users/classes/fetcher.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/>. + +/** + * File containing onlineusers class. + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_online_users; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class used to list and count online users + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class fetcher { + + /** @var string The SQL query for retrieving a list of online users */ + public $sql; + /** @var string The SQL query for counting the number of online users */ + public $csql; + /** @var string The params for the SQL queries */ + public $params; + + /** + * Class constructor + * + * @param int $currentgroup The group (if any) to filter on + * @param int $now Time now + * @param int $timetoshowusers Number of seconds to show online users + * @param context $context Context object used to generate the sql for users enrolled in a specific course + * @param bool $sitelevel Whether to check online users at site level. + * @param int $courseid The course id to check + */ + public function __construct($currentgroup, $now, $timetoshowusers, $context, $sitelevel = true, $courseid = null) { + $this->set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid); + } + + /** + * Store the SQL queries & params for listing online users + * + * @param int $currentgroup The group (if any) to filter on + * @param int $now Time now + * @param int $timetoshowusers Number of seconds to show online users + * @param context $context Context object used to generate the sql for users enrolled in a specific course + * @param bool $sitelevel Whether to check online users at site level. + * @param int $courseid The course id to check + */ + protected function set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid) { + $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache. + + $groupmembers = ""; + $groupselect = ""; + $groupby = ""; + $lastaccess = ", lastaccess"; + $timeaccess = ", ul.timeaccess AS lastaccess"; + $params = array(); + + $userfields = \user_picture::fields('u', array('username')); + + // Add this to the SQL to show only group users. + if ($currentgroup !== null) { + $groupmembers = ", {groups_members} gm"; + $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup"; + $groupby = "GROUP BY $userfields"; + $lastaccess = ", MAX(u.lastaccess) AS lastaccess"; + $timeaccess = ", MAX(ul.timeaccess) AS lastaccess"; + $params['currentgroup'] = $currentgroup; + } + + $params['now'] = $now; + $params['timefrom'] = $timefrom; + if ($sitelevel) { + $sql = "SELECT $userfields $lastaccess + FROM {user} u $groupmembers + WHERE u.lastaccess > :timefrom + AND u.lastaccess <= :now + AND u.deleted = 0 + $groupselect $groupby + ORDER BY lastaccess DESC "; + + $csql = "SELECT COUNT(u.id) + FROM {user} u $groupmembers + WHERE u.lastaccess > :timefrom + AND u.lastaccess <= :now + AND u.deleted = 0 + $groupselect"; + + } else { + // Course level - show only enrolled users for now. + // TODO: add a new capability for viewing of all users (guests+enrolled+viewing). + list($esqljoin, $eparams) = get_enrolled_sql($context); + $params = array_merge($params, $eparams); + + $sql = "SELECT $userfields $timeaccess + FROM {user_lastaccess} ul $groupmembers, {user} u + JOIN ($esqljoin) euj ON euj.id = u.id + WHERE ul.timeaccess > :timefrom + AND u.id = ul.userid + AND ul.courseid = :courseid + AND ul.timeaccess <= :now + AND u.deleted = 0 + $groupselect $groupby + ORDER BY lastaccess DESC"; + + $csql = "SELECT COUNT(u.id) + FROM {user_lastaccess} ul $groupmembers, {user} u + JOIN ($esqljoin) euj ON euj.id = u.id + WHERE ul.timeaccess > :timefrom + AND u.id = ul.userid + AND ul.courseid = :courseid + AND ul.timeaccess <= :now + AND u.deleted = 0 + $groupselect"; + + $params['courseid'] = $courseid; + } + $this->sql = $sql; + $this->csql = $csql; + $this->params = $params; + } + + /** + * Get a list of the most recent online users + * + * @param int $userlimit The maximum number of users that will be returned (optional, unlimited if not set) + * @return array + */ + public function get_users($userlimit = 0) { + global $DB; + $users = $DB->get_records_sql($this->sql, $this->params, 0, $userlimit); + return $users; + } + + /** + * Count the number of online users + * + * @return int + */ + public function count_users() { + global $DB; + return $DB->count_records_sql($this->csql, $this->params); + } + +} diff --git a/online_users/classes/privacy/provider.php b/online_users/classes/privacy/provider.php new file mode 100644 index 0000000..50d0280 --- /dev/null +++ b/online_users/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_online_users. + * + * @package block_online_users + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_online_users\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_online_users implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/online_users/db/access.php b/online_users/db/access.php new file mode 100644 index 0000000..f238b73 --- /dev/null +++ b/online_users/db/access.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/>. + +/** + * Online users block caps. + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/online_users:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/online_users:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + + 'block/online_users:viewlist' => array( + + 'captype' => 'read', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'user' => CAP_ALLOW, + 'guest' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ) +); diff --git a/online_users/lang/en/block_online_users.php b/online_users/lang/en/block_online_users.php new file mode 100644 index 0000000..ff79ce5 --- /dev/null +++ b/online_users/lang/en/block_online_users.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/>. + +/** + * Strings for component 'block_online_users', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.'; +$string['nouser'] = 'No online users'; +$string['numuser'] = '{$a} online user'; +$string['numusers'] = '{$a} online users'; +$string['online_users:addinstance'] = 'Add a new online users block'; +$string['online_users:myaddinstance'] = 'Add a new online users block to Dashboard'; +$string['online_users:viewlist'] = 'View list of online users'; +$string['periodnminutes'] = 'last {$a} minutes'; +$string['pluginname'] = 'Online users'; +$string['timetosee'] = 'Remove after inactivity (minutes)'; +$string['privacy:metadata'] = 'The Online users block only shows data stored in other locations.'; diff --git a/online_users/settings.php b/online_users/settings.php new file mode 100644 index 0000000..b9d7d81 --- /dev/null +++ b/online_users/settings.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/>. + +/** + * Online users block settings. + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtext('block_online_users_timetosee', get_string('timetosee', 'block_online_users'), + get_string('configtimetosee', 'block_online_users'), 5, PARAM_INT)); +} + diff --git a/online_users/styles.css b/online_users/styles.css new file mode 100644 index 0000000..90be203 --- /dev/null +++ b/online_users/styles.css @@ -0,0 +1,21 @@ +.block_online_users .content .list li.listentry { + clear: both; +} + +.block_online_users .content .list li.listentry .user { + float: left; + position: relative; +} + +.block_online_users .content .list li.listentry .user .userpicture { + vertical-align: text-bottom; +} + +.block_online_users .content .list li.listentry .message { + float: right; + margin-top: 3px; +} + +.block_online_users .content .info { + text-align: center; +} diff --git a/online_users/tests/behat/block_online_users_course.feature b/online_users/tests/behat/block_online_users_course.feature new file mode 100644 index 0000000..e86e4d8 --- /dev/null +++ b/online_users/tests/behat/block_online_users_course.feature @@ -0,0 +1,41 @@ +@block @block_online_users +Feature: The online users block allow you to see who is currently online + In order to enable the online users block on an course page + As a teacher + I can add the online users block to a course page + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + 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 | + + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + + Scenario: Add the online users on course page and see myself + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Online users" block + Then I should see "Teacher 1" in the "Online users" "block" + And I should see "1 online user" in the "Online users" "block" + + Scenario: Add the online users on course page and see other logged in users + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Online users" block + And I log out + And I log in as "student2" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Teacher 1" in the "Online users" "block" + And I should see "Student 1" in the "Online users" "block" + And I should not see "Student 2" in the "Online users" "block" + And I should see "2 online users" in the "Online users" "block" diff --git a/online_users/tests/behat/block_online_users_dashboard.feature b/online_users/tests/behat/block_online_users_dashboard.feature new file mode 100644 index 0000000..34207c5 --- /dev/null +++ b/online_users/tests/behat/block_online_users_dashboard.feature @@ -0,0 +1,28 @@ +@block @block_online_users +Feature: The online users block allow you to see who is currently online on dashboard + In order to use the online users block on the dashboard + As a user + I can view the online users block on my dashboard + + 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 | + + Scenario: View the online users block on the dashboard and see myself + Given I log in as "teacher1" + Then I should see "Teacher 1" in the "Online users" "block" + And I should see "1 online user" in the "Online users" "block" + + Scenario: View the online users block on the dashboard and see other logged in users + Given I log in as "student2" + And I log out + And I log in as "student1" + And I log out + When I log in as "teacher1" + Then I should see "Teacher 1" in the "Online users" "block" + And I should see "Student 1" in the "Online users" "block" + And I should see "Student 2" in the "Online users" "block" + And I should see "3 online users" in the "Online users" "block" diff --git a/online_users/tests/behat/block_online_users_frontpage.feature b/online_users/tests/behat/block_online_users_frontpage.feature new file mode 100644 index 0000000..6237d3d --- /dev/null +++ b/online_users/tests/behat/block_online_users_frontpage.feature @@ -0,0 +1,51 @@ +@block @block_online_users +Feature: The online users block allow you to see who is currently online on frontpage + In order to enable the online users block on the front page page + As an admin + I can add the online users block to the front page page + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + + Scenario: View the online users block on the front page and see myself + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + When I add the "Online users" block + Then I should see "Admin User" in the "Online users" "block" + And I should see "1 online user" in the "Online users" "block" + + Scenario: View the online users block on the front page as a logged in user + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Online users" block + And I log out + And I log in as "student2" + And I log out + When I log in as "student1" + And I am on site homepage + Then I should see "Admin User" in the "Online users" "block" + And I should see "Student 1" in the "Online users" "block" + And I should see "Student 2" in the "Online users" "block" + And I should see "3 online users" in the "Online users" "block" + + Scenario: View the online users block on the front page as a guest + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Online users" block + And I log out + And I log in as "student2" + And I log out + And I log in as "student1" + And I log out + When I log in as "guest" + And I am on site homepage + Then I should see "Admin User" in the "Online users" "block" + And I should see "Student 1" in the "Online users" "block" + And I should see "Student 2" in the "Online users" "block" + And I should see "3 online users" in the "Online users" "block" diff --git a/online_users/tests/generator/lib.php b/online_users/tests/generator/lib.php new file mode 100644 index 0000000..6b721a9 --- /dev/null +++ b/online_users/tests/generator/lib.php @@ -0,0 +1,90 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_online_users data generator + * + * @package block_online_users + * @category test + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Online users block data generator class + * + * @package block_online_users + * @category test + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_online_users_generator extends testing_block_generator { + + /** + * Create (simulated) logged in users and add some of them to groups in a course + */ + public function create_logged_in_users() { + global $DB; + + $generator = advanced_testcase::getDataGenerator(); + $data = array(); + + // Create 2 courses. + $course1 = $generator->create_course(); + $data['course1'] = $course1; + $course2 = $generator->create_course(); + $data['course2'] = $course2; + + // Create 9 (simulated) logged in users enroled into $course1. + for ($i = 1; $i <= 9; $i++) { + $user = $generator->create_user(); + $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id)); + $generator->enrol_user($user->id, $course1->id); + $DB->insert_record('user_lastaccess', array('userid' => $user->id, 'courseid' => $course1->id, 'timeaccess' => time())); + $data['user' . $i] = $user; + } + // Create 3 (simulated) logged in users who are not enroled into $course1. + for ($i = 10; $i <= 12; $i++) { + $user = $generator->create_user(); + $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id)); + $data['user' . $i] = $user; + } + + // Create 3 groups in course 1. + $group1 = $generator->create_group(array('courseid' => $course1->id)); + $data['group1'] = $group1; + $group2 = $generator->create_group(array('courseid' => $course1->id)); + $data['group2'] = $group2; + $group3 = $generator->create_group(array('courseid' => $course1->id)); + $data['group3'] = $group3; + + // Add 3 users to course group 1. + $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user1']->id)); + $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user2']->id)); + $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user3']->id)); + + // Add 4 users to course group 2. + $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user3']->id)); + $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user4']->id)); + $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user5']->id)); + $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user6']->id)); + + return $data; // Return the user, course and group objects. + } +} diff --git a/online_users/tests/generator_test.php b/online_users/tests/generator_test.php new file mode 100644 index 0000000..1ffcaba --- /dev/null +++ b/online_users/tests/generator_test.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/>. + +/** + * PHPUnit data generator tests + * + * @package block_online_users + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * PHPUnit data generator testcase + * + * @package block_online_users + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_online_users_generator_testcase extends advanced_testcase { + public function test_generator() { + global $DB; + + $this->resetAfterTest(true); + + $beforeblocks = $DB->count_records('block_instances'); + $beforecontexts = $DB->count_records('context'); + + /** @var block_online_users_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users'); + $this->assertInstanceOf('block_online_users_generator', $generator); + $this->assertEquals('online_users', $generator->get_blockname()); + + $generator->create_instance(); + $generator->create_instance(); + $bi = $generator->create_instance(); + $this->assertEquals($beforeblocks+3, $DB->count_records('block_instances')); + + } +} diff --git a/online_users/tests/online_users_test.php b/online_users/tests/online_users_test.php new file mode 100644 index 0000000..9ff1e3c --- /dev/null +++ b/online_users/tests/online_users_test.php @@ -0,0 +1,151 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Online users tests + * + * @package block_online_users + * @category test + * @copyright 2015 University of Nottingham <www.nottingham.ac.uk> + * @author Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use block_online_users\fetcher; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Online users testcase + * + * @package block_online_users + * @category test + * @copyright 2015 University of Nottingham <www.nottingham.ac.uk> + * @author Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_online_users_testcase extends advanced_testcase { + + protected $data; + + /** + * Tests initial setup. + * + * Prepare the site with some courses, groups, users and + * simulate various recent accesses. + */ + protected function setUp() { + + // Generate (simulated) recently logged-in users. + $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users'); + $this->data = $generator->create_logged_in_users(); + + // Confirm we have modified the site and requires reset. + $this->resetAfterTest(true); + } + + /** + * Check logged in group 1, 2 & 3 members in course 1 (should be 3, 4 and 0). + * + * @param array $data Array of user, course and group objects + * @param int $now Current Unix timestamp + * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users + */ + public function test_fetcher_course1_group_members() { + global $CFG; + + $groupid = $this->data['group1']->id; + $now = time(); + $timetoshowusers = $CFG->block_online_users_timetosee * 60; + $context = context_course::instance($this->data['course1']->id); + $courseid = $this->data['course1']->id; + $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals(3, $usercount, 'There was a problem counting the number of online users in group 1'); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 1'); + + $groupid = $this->data['group2']->id; + $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 2'); + $this->assertEquals(4, $usercount, 'There was a problem counting the number of online users in group 2'); + + $groupid = $this->data['group3']->id; + $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 3'); + $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in group 3'); + } + + /** + * Check logged in users in courses 1 & 2 (should be 9 and 0). + * + * @param array $data Array of user, course and group objects + * @param int $now Current Unix timestamp + * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users + */ + public function test_fetcher_courses() { + + global $CFG; + + $currentgroup = null; + $now = time(); + $timetoshowusers = $CFG->block_online_users_timetosee * 60; + $context = context_course::instance($this->data['course1']->id); + $courseid = $this->data['course1']->id; + $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 1'); + $this->assertEquals(9, $usercount, 'There was a problem counting the number of online users in course 1'); + + $courseid = $this->data['course2']->id; + $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 2'); + $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in course 2'); + } + + /** + * Check logged in at the site level (should be 12). + * + * @param int $now Current Unix timestamp + * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users + */ + public function test_fetcher_sitelevel() { + global $CFG; + + $currentgroup = null; + $now = time(); + $timetoshowusers = $CFG->block_online_users_timetosee * 60; + $context = context_system::instance(); + $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, true); + + $usercount = $onlineusers->count_users(); + $users = $onlineusers->get_users(); + $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users at site level'); + $this->assertEquals(12, $usercount, 'There was a problem counting the number of online users at site level'); + } +} diff --git a/online_users/version.php b/online_users/version.php new file mode 100644 index 0000000..6bbce9f --- /dev/null +++ b/online_users/version.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/>. + +/** + * Version details + * + * @package block_online_users + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_online_users'; // Full name of the plugin (used for diagnostics) diff --git a/participants/block_participants.php b/participants/block_participants.php new file mode 100644 index 0000000..b1b433b --- /dev/null +++ b/participants/block_participants.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/>. + +/** + * Participants block + * + * @package block_participants + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/course/lib.php'); + +/** + * Participants block + * + * @package block_participants + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_participants extends block_list { + function init() { + $this->title = get_string('pluginname', 'block_participants'); + } + + 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 = ''; + + // user/index.php expect course context, so get one if page has module context. + $currentcontext = $this->page->context->get_course_context(false); + + if (empty($currentcontext)) { + $this->content = ''; + return $this->content; + } else if ($this->page->course->id == SITEID) { + if (!course_can_view_participants(context_system::instance())) { + $this->content = ''; + return $this->content; + } + } else { + if (!course_can_view_participants($currentcontext)) { + $this->content = ''; + return $this->content; + } + } + + $icon = $OUTPUT->pix_icon('i/users', ''); + $this->content->items[] = '<a title="'.get_string('listofallpeople').'" href="'. + $CFG->wwwroot.'/user/index.php?contextid='.$currentcontext->id.'">'.$icon.get_string('participants').'</a>'; + + return $this->content; + } + + // my moodle can only have SITEID and it's redundant here, so take it away + function applicable_formats() { + return array('all' => true, 'my' => false, 'tag' => false); + } + +} diff --git a/participants/classes/privacy/provider.php b/participants/classes/privacy/provider.php new file mode 100644 index 0000000..efcffd1 --- /dev/null +++ b/participants/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_participants. + * + * @package block_participants + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_participants\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_participants implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/participants/db/access.php b/participants/db/access.php new file mode 100644 index 0000000..734f8a1 --- /dev/null +++ b/participants/db/access.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/>. + +/** + * Participants block caps. + * + * @package block_participants + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/participants: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/participants/lang/en/block_participants.php b/participants/lang/en/block_participants.php new file mode 100644 index 0000000..20eb2dd --- /dev/null +++ b/participants/lang/en/block_participants.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/>. + +/** + * Strings for component 'block_participants', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_participants + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['participants:addinstance'] = 'Add a new people block'; +$string['pluginname'] = 'People'; +$string['privacy:metadata'] = 'The People block only shows data stored in other locations.'; diff --git a/participants/tests/behat/block_participants_course.feature b/participants/tests/behat/block_participants_course.feature new file mode 100644 index 0000000..5fa4a0b --- /dev/null +++ b/participants/tests/behat/block_participants_course.feature @@ -0,0 +1,40 @@ +@block @block_participants +Feature: People Block used in a course + In order to view participants in a course + As a teacher + I can add the people block to a course + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C101 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam | Student | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C101 | student | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I add the "People" block + And I log out + + Scenario: Student can view participants link + When I log in as "student1" + And I am on "Course 1" course homepage + Then "People" "block" should exist + And I should see "Participants" in the "People" "block" + + Scenario: Student can follow participants link and be directed to the correct page + When I log in as "student1" + And I am on "Course 1" course homepage + And I click on "Participants" "link" in the "People" "block" + Then I should see "Participants" in the "#page-content" "css_element" + + Scenario: Student without permission can not view participants link + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/course:viewparticipants | Prevent | student | Course | C101 | + When I log in as "student1" + And I am on "Course 1" course homepage + Then "People" "block" should not exist diff --git a/participants/tests/behat/block_participants_frontpage.feature b/participants/tests/behat/block_participants_frontpage.feature new file mode 100644 index 0000000..93b6a57 --- /dev/null +++ b/participants/tests/behat/block_participants_frontpage.feature @@ -0,0 +1,26 @@ +@block @block_participants +Feature: People Block used on frontpage + In order to view participants in a site + As a admin + I can add the people block to the front page + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam | Student | student1@example.com | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "People" block + And I log out + + Scenario: Admin can view site participants link + When I log in as "admin" + And I am on site homepage + Then "People" "block" should exist + And I should see "Participants" in the "People" "block" + + Scenario: Student can not follow participants link on frontpage + When I log in as "student1" + And I am on site homepage + Then "People" "block" should not exist diff --git a/participants/version.php b/participants/version.php new file mode 100644 index 0000000..d05b247 --- /dev/null +++ b/participants/version.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/>. + +/** + * Version details + * + * @package block_participants + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_participants'; // Full name of the plugin (used for diagnostics) diff --git a/private_files/block_private_files.php b/private_files/block_private_files.php new file mode 100644 index 0000000..fba8113 --- /dev/null +++ b/private_files/block_private_files.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/>. + + +/** + * Manage user private area files + * + * @package block_private_files + * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_private_files extends block_base { + + function init() { + $this->title = get_string('pluginname', 'block_private_files'); + } + + function specialization() { + } + + function applicable_formats() { + return array('all' => true); + } + + function instance_allow_multiple() { + return false; + } + + function get_content() { + global $CFG, $USER, $PAGE, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + if (empty($this->instance)) { + return null; + } + + $this->content = new stdClass(); + $this->content->text = ''; + $this->content->footer = ''; + if (isloggedin() && !isguestuser()) { // Show the block + $this->content = new stdClass(); + + //TODO: add capability check here! + + $renderer = $this->page->get_renderer('block_private_files'); + $this->content->text = $renderer->private_files_tree(); + if (has_capability('moodle/user:manageownfiles', $this->context)) { + $this->content->footer = html_writer::link( + new moodle_url('/user/files.php', array('returnurl' => $PAGE->url->out())), + get_string('privatefilesmanage') . '...'); + } + + } + return $this->content; + } +} diff --git a/private_files/classes/privacy/provider.php b/private_files/classes/privacy/provider.php new file mode 100644 index 0000000..147ab44 --- /dev/null +++ b/private_files/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_private_files. + * + * @package block_private_files + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_private_files\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_private_files implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/private_files/db/access.php b/private_files/db/access.php new file mode 100644 index 0000000..707027f --- /dev/null +++ b/private_files/db/access.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/>. + +/** + * Private files block caps. + * + * @package block_private_files + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/private_files:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/private_files: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/private_files/edit.php b/private_files/edit.php new file mode 100644 index 0000000..785349e --- /dev/null +++ b/private_files/edit.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/>. + +/** + * Manage files in folder in private area. + * + * This page is not used and now redirects to the page to manage the private files. + * + * @package block_private_files + * @copyright 2010 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); + +redirect(new moodle_url('/user/files.php')); diff --git a/private_files/lang/en/block_private_files.php b/private_files/lang/en/block_private_files.php new file mode 100644 index 0000000..f0791fc --- /dev/null +++ b/private_files/lang/en/block_private_files.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/>. + +/** + * Strings for component 'block_private_files', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_private_files + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Private files'; +$string['privatefiles'] = 'Private files'; +$string['private_files:addinstance'] = 'Add a new private files block'; +$string['private_files:myaddinstance'] = 'Add a new private files block to Dashboard'; +$string['privacy:metadata'] = 'The Private files block only provides a view of, and a link to, the user\'s private files.'; diff --git a/private_files/module.js b/private_files/module.js new file mode 100644 index 0000000..42a4026 --- /dev/null +++ b/private_files/module.js @@ -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/>. + +/** + * Init private files treeview + * + * @package block_private_files + * @copyright 2009 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +M.block_private_files = {}; + +M.block_private_files.init_tree = function(Y, expand_all, htmlid) { + Y.use('yui2-treeview', function(Y) { + var tree = new Y.YUI2.widget.TreeView(htmlid); + + tree.subscribe("clickEvent", function(node, event) { + // we want normal clicking which redirects to url + return false; + }); + + if (expand_all) { + tree.expandAll(); + } + + tree.render(); + }); +}; diff --git a/private_files/renderer.php b/private_files/renderer.php new file mode 100644 index 0000000..d357053 --- /dev/null +++ b/private_files/renderer.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/>. + +/** + * Print private files tree + * + * @package block_private_files + * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +class block_private_files_renderer extends plugin_renderer_base { + + /** + * Prints private files tree view + * @return string + */ + public function private_files_tree() { + return $this->render(new private_files_tree); + } + + public function render_private_files_tree(private_files_tree $tree) { + $module = array('name'=>'block_private_files', 'fullpath'=>'/blocks/private_files/module.js', 'requires'=>array('yui2-treeview')); + if (empty($tree->dir['subdirs']) && empty($tree->dir['files'])) { + $html = $this->output->box(get_string('nofilesavailable', 'repository')); + } else { + $htmlid = 'private_files_tree_'.uniqid(); + $this->page->requires->js_init_call('M.block_private_files.init_tree', array(false, $htmlid)); + $html = '<div id="'.$htmlid.'">'; + $html .= $this->htmllize_tree($tree, $tree->dir); + $html .= '</div>'; + } + + return $html; + } + + /** + * Internal function - creates htmls structure suitable for YUI tree. + */ + protected function htmllize_tree($tree, $dir) { + global $CFG; + $yuiconfig = array(); + $yuiconfig['type'] = 'html'; + + if (empty($dir['subdirs']) and empty($dir['files'])) { + return ''; + } + $result = '<ul>'; + foreach ($dir['subdirs'] as $subdir) { + $image = $this->output->pix_icon(file_folder_icon(), $subdir['dirname'], 'moodle', array('class'=>'icon')); + $result .= '<li yuiConfig=\''.json_encode($yuiconfig).'\'><div>'.$image.s($subdir['dirname']).'</div> '.$this->htmllize_tree($tree, $subdir).'</li>'; + } + foreach ($dir['files'] as $file) { + $url = file_encode_url("$CFG->wwwroot/pluginfile.php", '/'.$tree->context->id.'/user/private'.$file->get_filepath().$file->get_filename(), true); + $filename = $file->get_filename(); + $image = $this->output->pix_icon(file_file_icon($file), $filename, 'moodle', array('class'=>'icon')); + $result .= '<li yuiConfig=\''.json_encode($yuiconfig).'\'><div>'.html_writer::link($url, $image.$filename).'</div></li>'; + } + $result .= '</ul>'; + + return $result; + } +} + +class private_files_tree implements renderable { + public $context; + public $dir; + public function __construct() { + global $USER; + $this->context = context_user::instance($USER->id); + $fs = get_file_storage(); + $this->dir = $fs->get_area_tree($this->context->id, 'user', 'private', 0); + } +} diff --git a/private_files/styles.css b/private_files/styles.css new file mode 100644 index 0000000..b553b4c --- /dev/null +++ b/private_files/styles.css @@ -0,0 +1,10 @@ +/* Rule so that the table tree view works with word-wrap: break-word. */ +.block_private_files .content table { + table-layout: fixed; + width: 100%; +} + +.block_private_files .content .footer { + padding: 10px 0 0; + margin-top: .5em; +} diff --git a/private_files/tests/behat/block_private_files_activity.feature b/private_files/tests/behat/block_private_files_activity.feature new file mode 100644 index 0000000..fb84377 --- /dev/null +++ b/private_files/tests/behat/block_private_files_activity.feature @@ -0,0 +1,28 @@ +@block @block_private_files @_file_upload @javascript +Feature: The private files block allows users to store files privately in moodle on activity page + In order to store a private file in moodle + As a teacher + I can upload the file to my private files area using the private files block in an activity + + Scenario: Upload a file to the private files block in an activity + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | course | idnumber | name | intro | + | page | C1 | page1 | Test page name | Test page description | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test page name" + And I add the "Private files" block + And I should see "No files available" in the "Private files" "block" + When I follow "Manage private files..." + And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager + And I press "Save changes" + Then I should see "testfile.txt" in the "Private files" "block" diff --git a/private_files/tests/behat/block_private_files_course.feature b/private_files/tests/behat/block_private_files_course.feature new file mode 100644 index 0000000..35a4e96 --- /dev/null +++ b/private_files/tests/behat/block_private_files_course.feature @@ -0,0 +1,24 @@ +@block @block_private_files @_file_upload @javascript +Feature: The private files block allows users to store files privately in moodle on course page + In order to store a private file in moodle + As a teacher + I can upload the file to my private files area using the private files block in a course + + Scenario: Upload a file to the private files block from a course + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Private files" block + And I should see "No files available" in the "Private files" "block" + When I follow "Manage private files..." + And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager + And I press "Save changes" + Then I should see "testfile.txt" in the "Private files" "block" diff --git a/private_files/tests/behat/block_private_files_dashboard.feature b/private_files/tests/behat/block_private_files_dashboard.feature new file mode 100644 index 0000000..3b6eb5c --- /dev/null +++ b/private_files/tests/behat/block_private_files_dashboard.feature @@ -0,0 +1,17 @@ +@block @block_private_files @_file_upload @javascript +Feature: The private files block allows users to store files privately in moodle on dashboard + In order to store a private file in moodle + As a user + I can upload the file to my private files area using the private files block on the dashboard + + Scenario: Upload a file to the private files block from the dashboard + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And I log in as "teacher1" + And "Private files" "block" should exist + And I should see "No files available" in the "Private files" "block" + When I follow "Manage private files..." + And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager + And I press "Save changes" + Then I should see "testfile.txt" in the "Private files" "block" diff --git a/private_files/tests/behat/block_private_files_frontpage.feature b/private_files/tests/behat/block_private_files_frontpage.feature new file mode 100644 index 0000000..7816060 --- /dev/null +++ b/private_files/tests/behat/block_private_files_frontpage.feature @@ -0,0 +1,34 @@ +@block @block_private_files @_file_upload +Feature: The private files block allows users to store files privately in moodle on front page. + In order to store a private file in moodle + As a teacher + I can upload the file to my private files area using the private files block from the front page + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Private files" block + And I log out + + Scenario: Try to view the private files block as a guest + Given I log in as "guest" + When I am on site homepage + Then "Private files" "block" should not exist + + @javascript + Scenario: Upload a file to the private files block from the frontpage + Given I log in as "teacher1" + And I am on site homepage + And "Private files" "block" should exist + And I should see "No files available" in the "Private files" "block" + When I follow "Manage private files..." + And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager + And I press "Save changes" + Then I should see "testfile.txt" in the "Private files" "block" diff --git a/private_files/tests/fixtures/testfile.txt b/private_files/tests/fixtures/testfile.txt new file mode 100644 index 0000000..9f4b6d8 --- /dev/null +++ b/private_files/tests/fixtures/testfile.txt @@ -0,0 +1 @@ +This is a test file diff --git a/private_files/version.php b/private_files/version.php new file mode 100644 index 0000000..3376580 --- /dev/null +++ b/private_files/version.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/>. + +/** + * Version details + * + * @package block_private_files + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_private_files'; // Full name of the plugin (used for diagnostics) diff --git a/quiz_results/backup/moodle2/restore_quiz_results_block_task.class.php b/quiz_results/backup/moodle2/restore_quiz_results_block_task.class.php new file mode 100644 index 0000000..9aff839 --- /dev/null +++ b/quiz_results/backup/moodle2/restore_quiz_results_block_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/>. + +/** + * @package block_quiz_results + * @copyright 2003 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(); + +/** + * Specialised restore task for the quiz_results block + * (using execute_after_tasks for recoding of target quiz) + * + * TODO: Finish phpdocs + * + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_quiz_results_block_task extends restore_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + } + + public function get_fileareas() { + return array(); // No associated fileareas + } + + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata + } + + /** + * This function, executed after all the tasks in the plan + * have been executed, will perform the recode of the + * target quiz for the block. This must be done here + * and not in normal execution steps because the quiz + * can be restored after the block. + */ + public function after_restore() { + global $DB; + + // Get the blockid. + $blockid = $this->get_blockid(); + + // Extract block configdata and update it to point to the new quiz. + $configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid)); + $newconfigdata = ''; + + // The block was configured. + if (!empty($configdata)) { + + $config = unserialize(base64_decode($configdata)); + $config->activityparent = 'quiz'; + $config->activityparentid = 0; + $config->gradeformat = isset($config->gradeformat) ? $config->gradeformat : 1; + + if (!empty($config->quizid) + && $quizmap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'quiz', $config->quizid)) { + $config->activityparentid = $quizmap->newitemid; + } + + // Set the decimal valuue as appropriate. + if ($config->gradeformat == 1) { + // This block is using percentages, do not display any decimal places. + $config->decimalpoints = 0; + } else { + // Get the decimal value from the corresponding quiz. + $config->decimalpoints = $DB->get_field('quiz', 'decimalpoints', array('id' => $config->activityparentid)); + } + + // Get the grade_items record to set the activitygradeitemid. + $info = $DB->get_record('grade_items', + array('iteminstance' => $config->activityparentid, 'itemmodule' => $config->activityparent)); + $config->activitygradeitemid = 0; + if ($info) { + $config->activitygradeitemid = $info->id; + } + + unset($config->quizid); + $newconfigdata = base64_encode(serialize($config)); + } + + // Update the configuration and convert the block. + $DB->set_field('block_instances', 'configdata', $newconfigdata, array('id' => $blockid)); + $DB->set_field('block_instances', 'blockname', 'activity_results', array('id' => $blockid)); + } + + static public function define_decode_contents() { + return array(); + } + + static public function define_decode_rules() { + return array(); + } +} diff --git a/quiz_results/block_quiz_results.php b/quiz_results/block_quiz_results.php new file mode 100644 index 0000000..bfb259b --- /dev/null +++ b/quiz_results/block_quiz_results.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/>. + +/** + * Classes to enforce the various access rules that can apply to a quiz. + * + * @package block_quiz_results + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/lib.php'); + +/** + * Block quiz_results class definition. + * + * This block can be added to a course page or a quiz page to display of list of + * the best/worst students/groups in a particular quiz. + * + * @package block_quiz_results + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_quiz_results extends block_base { + function init() { + $this->title = get_string('pluginname', 'block_quiz_results'); + } + + function applicable_formats() { + return array('mod-quiz' => true); + } + + function instance_config_save($data, $nolongerused = false) { + parent::instance_config_save($data); + } + + function get_content() { + return $this->content; + } + + function instance_allow_multiple() { + return true; + } +} + + diff --git a/quiz_results/classes/privacy/provider.php b/quiz_results/classes/privacy/provider.php new file mode 100644 index 0000000..0994665 --- /dev/null +++ b/quiz_results/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_quiz_results. + * + * @package block_quiz_results + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_quiz_results\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_quiz_results implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/quiz_results/db/access.php b/quiz_results/db/access.php new file mode 100644 index 0000000..8e3622b --- /dev/null +++ b/quiz_results/db/access.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/>. + +/** + * Quiz results block caps. + * + * @package block_quiz_results + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/quiz_results: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/quiz_results/db/install.php b/quiz_results/db/install.php new file mode 100644 index 0000000..255cdba --- /dev/null +++ b/quiz_results/db/install.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/>. + +/** + * Quiz results block installation. + * + * @package block_quiz_results + * @copyright 2015 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +function xmldb_block_quiz_results_install() { + global $DB; + + // Disable quiz_results on new installs (its now just a stub). + $DB->set_field('block', 'visible', 0, array('name' => 'quiz_results')); +} + diff --git a/quiz_results/db/upgrade.php b/quiz_results/db/upgrade.php new file mode 100644 index 0000000..18ee857 --- /dev/null +++ b/quiz_results/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/>. + +/** + * This file keeps track of upgrades to the quiz_results block + * + * 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. + * + * @since Moodle 2.9 + * @package block_quiz_results + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade the quiz_results block + * @param int $oldversion + * @param object $block + */ +function xmldb_block_quiz_results_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/quiz_results/lang/en/block_quiz_results.php b/quiz_results/lang/en/block_quiz_results.php new file mode 100644 index 0000000..f822a2f --- /dev/null +++ b/quiz_results/lang/en/block_quiz_results.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/>. + +/** + * Strings for component 'block_quiz_results', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_quiz_results + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Quiz results (disabled)'; +$string['quiz_results:addinstance'] = 'Add a new quiz results block'; +$string['privacy:metadata'] = 'The Quiz results block only shows data stored in other locations.'; diff --git a/quiz_results/lang/en/depreciated.txt b/quiz_results/lang/en/depreciated.txt new file mode 100644 index 0000000..e69de29 diff --git a/quiz_results/version.php b/quiz_results/version.php new file mode 100644 index 0000000..b3ae74a --- /dev/null +++ b/quiz_results/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 for the block_quiz_results plugin. + * + * @package block_quiz_results + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_quiz_results'; // Full name of the plugin (used for diagnostics) + +$plugin->dependencies = array('mod_quiz' => 2018050800); diff --git a/recent_activity/block_recent_activity.php b/recent_activity/block_recent_activity.php new file mode 100644 index 0000000..6a46c36 --- /dev/null +++ b/recent_activity/block_recent_activity.php @@ -0,0 +1,309 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_recent_activity + * + * @package block_recent_activity + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/course/lib.php'); + +/** + * class block_recent_activity + * + * @package block_recent_activity + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_recent_activity extends block_base { + + /** + * Use {@link block_recent_activity::get_timestart()} to access + * + * @var int stores the time since when we want to show recent activity + */ + protected $timestart = null; + + /** + * Initialises the block + */ + function init() { + $this->title = get_string('pluginname', 'block_recent_activity'); + } + + /** + * Returns the content object + * + * @return stdObject + */ + function get_content() { + if ($this->content !== NULL) { + return $this->content; + } + + if (empty($this->instance)) { + $this->content = ''; + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + $renderer = $this->page->get_renderer('block_recent_activity'); + $this->content->text = $renderer->recent_activity($this->page->course, + $this->get_timestart(), + $this->get_recent_enrolments(), + $this->get_structural_changes(), + $this->get_modules_recent_activity()); + + return $this->content; + } + + /** + * Returns the time since when we want to show recent activity + * + * For guest users it is 2 days, for registered users it is the time of last access to the course + * + * @return int + */ + protected function get_timestart() { + global $USER; + if ($this->timestart === null) { + $this->timestart = round(time() - COURSE_MAX_RECENT_PERIOD, -2); // better db caching for guests - 100 seconds + + if (!isguestuser()) { + if (!empty($USER->lastcourseaccess[$this->page->course->id])) { + if ($USER->lastcourseaccess[$this->page->course->id] > $this->timestart) { + $this->timestart = $USER->lastcourseaccess[$this->page->course->id]; + } + } + } + } + return $this->timestart; + } + + /** + * Returns all recent enrolments. + * + * This function previously used get_recent_enrolments located in lib/deprecatedlib.php which would + * return an empty array which was identified in MDL-36993. The use of this function outside the + * deprecated lib was removed in MDL-40649. + * + * @todo MDL-36993 this function always return empty array + * @return array array of entries from {user} table + */ + protected function get_recent_enrolments() { + return array(); + } + + /** + * Returns list of recent changes in course structure + * + * It includes adding, editing or deleting of the resources or activities + * Excludes changes on modules without a view link (i.e. labels), and also + * if activity was both added and deleted + * + * @return array array of changes. Each element is an array containing attributes: + * 'action' - one of: 'add mod', 'update mod', 'delete mod' + * 'module' - instance of cm_info (for 'delete mod' it is an object with attributes modname and modfullname) + */ + protected function get_structural_changes() { + global $DB; + $course = $this->page->course; + $context = context_course::instance($course->id); + $canviewdeleted = has_capability('block/recent_activity:viewdeletemodule', $context); + $canviewupdated = has_capability('block/recent_activity:viewaddupdatemodule', $context); + if (!$canviewdeleted && !$canviewupdated) { + return; + } + + $timestart = $this->get_timestart(); + $changelist = array(); + // The following query will retrieve the latest action for each course module in the specified course. + // Also the query filters out the modules that were created and then deleted during the given interval. + $sql = "SELECT + cmid, MIN(action) AS minaction, MAX(action) AS maxaction, MAX(modname) AS modname + FROM {block_recent_activity} + WHERE timecreated > ? AND courseid = ? + GROUP BY cmid + ORDER BY MAX(timecreated) ASC"; + $params = array($timestart, $course->id); + $logs = $DB->get_records_sql($sql, $params); + if (isset($logs[0])) { + // If special record for this course and cmid=0 is present, migrate logs. + self::migrate_logs($course); + $logs = $DB->get_records_sql($sql, $params); + } + if ($logs) { + $modinfo = get_fast_modinfo($course); + foreach ($logs as $log) { + // We used aggregate functions since constants CM_CREATED, CM_UPDATED and CM_DELETED have ascending order (0,1,2). + $wasdeleted = ($log->maxaction == block_recent_activity_observer::CM_DELETED); + $wascreated = ($log->minaction == block_recent_activity_observer::CM_CREATED); + + if ($wasdeleted && $wascreated) { + // Activity was created and deleted within this interval. Do not show it. + continue; + } else if ($wasdeleted && $canviewdeleted) { + if (plugin_supports('mod', $log->modname, FEATURE_NO_VIEW_LINK, false)) { + // Better to call cm_info::has_view() because it can be dynamic. + // But there is no instance of cm_info now. + continue; + } + // Unfortunately we do not know if the mod was visible. + $modnames = get_module_types_names(); + $changelist[$log->cmid] = array('action' => 'delete mod', + 'module' => (object)array( + 'modname' => $log->modname, + 'modfullname' => isset($modnames[$log->modname]) ? $modnames[$log->modname] : $log->modname + )); + + } else if (!$wasdeleted && isset($modinfo->cms[$log->cmid]) && $canviewupdated) { + // Module was either added or updated during this interval and it currently exists. + // If module was both added and updated show only "add" action. + $cm = $modinfo->cms[$log->cmid]; + if ($cm->has_view() && $cm->uservisible) { + $changelist[$log->cmid] = array( + 'action' => $wascreated ? 'add mod' : 'update mod', + 'module' => $cm + ); + } + } + } + } + return $changelist; + } + + /** + * Returns list of recent activity within modules + * + * For each used module type executes callback MODULE_print_recent_activity() + * + * @return array array of pairs moduletype => content + */ + protected function get_modules_recent_activity() { + $context = context_course::instance($this->page->course->id); + $viewfullnames = has_capability('moodle/site:viewfullnames', $context); + $hascontent = false; + + $modinfo = get_fast_modinfo($this->page->course); + $usedmodules = $modinfo->get_used_module_names(); + $recentactivity = array(); + foreach ($usedmodules as $modname => $modfullname) { + // Each module gets it's own logs and prints them + ob_start(); + $hascontent = component_callback('mod_'. $modname, 'print_recent_activity', + array($this->page->course, $viewfullnames, $this->get_timestart()), false); + if ($hascontent) { + $recentactivity[$modname] = ob_get_contents(); + } + ob_end_clean(); + } + return $recentactivity; + } + + /** + * Which page types this block may appear on. + * + * @return array page-type prefix => true/false. + */ + function applicable_formats() { + return array('all' => true, 'my' => false, 'tag' => false); + } + + /** + * Remove old entries from table block_recent_activity + */ + public function cron() { + global $DB; + // Those entries will never be displayed as RECENT anyway. + $DB->delete_records_select('block_recent_activity', 'timecreated < ?', + array(time() - COURSE_MAX_RECENT_PERIOD)); + } + + /** + * Migrates entries from table {log} into {block_recent_activity} + * + * We only migrate logs for the courses that actually have recent activity + * block and that are being viewed within COURSE_MAX_RECENT_PERIOD time + * after the upgrade. + * + * The presence of entry in {block_recent_activity} with the cmid=0 indicates + * that the course needs log migration. Those entries were installed in + * db/upgrade.php when the table block_recent_activity was created. + * + * @param stdClass $course + */ + protected static function migrate_logs($course) { + global $DB; + if (!$logstarted = $DB->get_record('block_recent_activity', + array('courseid' => $course->id, 'cmid' => 0), + 'id, timecreated')) { + return; + } + $DB->delete_records('block_recent_activity', array('id' => $logstarted->id)); + try { + $logs = $DB->get_records_select('log', + "time > ? AND time < ? AND course = ? AND + module = 'course' AND + (action = 'add mod' OR action = 'update mod' OR action = 'delete mod')", + array(time()-COURSE_MAX_RECENT_PERIOD, $logstarted->timecreated, $course->id), + 'id ASC', 'id, time, userid, cmid, action, info'); + } catch (Exception $e) { + // Probably table {log} was already removed. + return; + } + if (!$logs) { + return; + } + $modinfo = get_fast_modinfo($course); + $entries = array(); + foreach ($logs as $log) { + $info = explode(' ', $log->info); + if (count($info) != 2) { + continue; + } + $modname = $info[0]; + $instanceid = $info[1]; + $entry = array('courseid' => $course->id, 'userid' => $log->userid, + 'timecreated' => $log->time, 'modname' => $modname); + if ($log->action == 'delete mod') { + if (!$log->cmid) { + continue; + } + $entry['action'] = 2; + $entry['cmid'] = $log->cmid; + } else { + if (!isset($modinfo->instances[$modname][$instanceid])) { + continue; + } + if ($log->action == 'add mod') { + $entry['action'] = 0; + } else { + $entry['action'] = 1; + } + $entry['cmid'] = $modinfo->instances[$modname][$instanceid]->id; + } + $entries[] = $entry; + } + $DB->insert_records('block_recent_activity', $entries); + } +} + diff --git a/recent_activity/classes/observer.php b/recent_activity/classes/observer.php new file mode 100644 index 0000000..ad55559 --- /dev/null +++ b/recent_activity/classes/observer.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/>. + +/** + * Event observer. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event observer. + * Stores all actions about modules create/update/delete in plugin own's table. + * This allows the block to avoid expensive queries to the log table. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_recent_activity_observer { + + /** @var int indicates that course module was created */ + const CM_CREATED = 0; + /** @var int indicates that course module was udpated */ + const CM_UPDATED = 1; + /** @var int indicates that course module was deleted */ + const CM_DELETED = 2; + + /** + * Store all actions about modules create/update/delete in own table. + * + * @param \core\event\base $event + */ + public static function store(\core\event\base $event) { + global $DB; + $eventdata = new \stdClass(); + switch ($event->eventname) { + case '\core\event\course_module_created': + $eventdata->action = self::CM_CREATED; + break; + case '\core\event\course_module_updated': + $eventdata->action = self::CM_UPDATED; + break; + case '\core\event\course_module_deleted': + $eventdata->action = self::CM_DELETED; + $eventdata->modname = $event->other['modulename']; + break; + default: + return; + } + $eventdata->timecreated = $event->timecreated; + $eventdata->courseid = $event->courseid; + $eventdata->cmid = $event->objectid; + $eventdata->userid = $event->userid; + $DB->insert_record('block_recent_activity', $eventdata); + } +} diff --git a/recent_activity/classes/privacy/provider.php b/recent_activity/classes/privacy/provider.php new file mode 100644 index 0000000..b024b5b --- /dev/null +++ b/recent_activity/classes/privacy/provider.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/>. + +/** + * Privacy subsystem implementation for block_recent_activity. + * + * @package block_recent_activity + * @category privacy + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_recent_activity\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The block_recent_activity does not keep any data for more than COURSE_MAX_RECENT_PERIOD. + * + * @copyright 2018 Shamim Rezaie <shamim@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { + + /** + * 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) : collection { + + // This plugin defines a db table but it is not considered personal data and, therefore, not exported or deleted. + $collection->add_database_table('block_recent_activity', [ + 'courseid' => 'privacy:metadata:block_recent_activity:courseid', + 'cmid' => 'privacy:metadata:block_recent_activity:cmid', + 'timecreated' => 'privacy:metadata:block_recent_activity:timecreated', + 'userid' => 'privacy:metadata:block_recent_activity:userid', + 'action' => 'privacy:metadata:block_recent_activity:action', + 'modname' => 'privacy:metadata:block_recent_activity:modname' + ], 'privacy:metadata:block_recent_activity'); + + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + return new contextlist(); + } + + /** + * 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) { + } + + /** + * 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) { + } + + /** + * 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) { + } +} diff --git a/recent_activity/db/access.php b/recent_activity/db/access.php new file mode 100644 index 0000000..80530bc --- /dev/null +++ b/recent_activity/db/access.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/>. + +/** + * Recent activity block caps. + * + * @package block_recent_activity + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/recent_activity:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + + 'block/recent_activity:viewaddupdatemodule' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + + 'block/recent_activity:viewdeletemodule' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ) +); diff --git a/recent_activity/db/events.php b/recent_activity/db/events.php new file mode 100644 index 0000000..0819cdb --- /dev/null +++ b/recent_activity/db/events.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/>. + +/** + * Event observer. + * + * @package block_recent_activity + * @category event + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = array ( + array ( + 'eventname' => '\core\event\course_module_created', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), + array ( + 'eventname' => '\core\event\course_module_updated', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), + array ( + 'eventname' => '\core\event\course_module_deleted', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), +); diff --git a/recent_activity/db/install.xml b/recent_activity/db/install.xml new file mode 100644 index 0000000..34fc9ff --- /dev/null +++ b/recent_activity/db/install.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="blocks/recent_activity/db" VERSION="20140120" COMMENT="XMLDB file for Moodle blocks/recent_activity" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="block_recent_activity" COMMENT="Recent activity block"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Course id"/> + <FIELD NAME="cmid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Course module id"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User performing the action"/> + <FIELD NAME="action" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="0 created, 1 updated, 2 deleted"/> + <FIELD NAME="modname" TYPE="char" LENGTH="20" NOTNULL="false" SEQUENCE="false" COMMENT="module type name (for delete action)"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="coursetime" UNIQUE="false" FIELDS="courseid, timecreated"/> + </INDEXES> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/recent_activity/db/upgrade.php b/recent_activity/db/upgrade.php new file mode 100644 index 0000000..d9bf407 --- /dev/null +++ b/recent_activity/db/upgrade.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 keeps track of upgrades to the recent activity block + * + * 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. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade code for the recent activity block. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_recent_activity_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/recent_activity/lang/en/block_recent_activity.php b/recent_activity/lang/en/block_recent_activity.php new file mode 100644 index 0000000..a569a7a --- /dev/null +++ b/recent_activity/lang/en/block_recent_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/>. + +/** + * Strings for component 'block_recent_activity', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_recent_activity + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Recent activity'; +$string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.'; +$string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days'; +$string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted'; +$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id'; +$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id'; +$string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)'; +$string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed'; +$string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action'; +$string['recent_activity:addinstance'] = 'Add a new recent activity block'; +$string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block'; +$string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block'; diff --git a/recent_activity/renderer.php b/recent_activity/renderer.php new file mode 100644 index 0000000..2e96eeb --- /dev/null +++ b/recent_activity/renderer.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/>. + +/** + * Renderer for block recent_activity + * + * @package block_recent_activity + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * recent_activity block rendrer + * + * @package block_recent_activity + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_recent_activity_renderer extends plugin_renderer_base { + + /** + * Renders HTML to display recent_activity block + * + * @param stdClass $course + * @param int $timestart + * @param array $recentenrolments array of changes in enrolments + * @param array $structuralchanges array of changes in course structure + * @param array $modulesrecentactivity array of changes in modules (provided by modules) + * @return string + */ + public function recent_activity($course, $timestart, $recentenrolments, $structuralchanges, + $modulesrecentactivity) { + + $output = html_writer::tag('div', + get_string('activitysince', '', userdate($timestart)), + array('class' => 'activityhead')); + + $output .= html_writer::tag('div', + html_writer::link(new moodle_url('/course/recent.php', array('id' => $course->id)), + get_string('recentactivityreport')), + array('class' => 'activityhead')); + + $content = false; + + // Firstly, have there been any new enrolments? + if ($recentenrolments) { + $content = true; + $context = context_course::instance($course->id); + $viewfullnames = has_capability('moodle/site:viewfullnames', $context); + $output .= html_writer::start_tag('div', array('class' => 'newusers')); + $output .= $this->heading(get_string("newusers").':', 3); + //Accessibility: new users now appear in an <OL> list. + $output .= html_writer::start_tag('ol', array('class' => 'list')); + foreach ($recentenrolments as $user) { + $output .= html_writer::tag('li', + html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)), + fullname($user, $viewfullnames)), + array('class' => 'name')); + } + $output .= html_writer::end_tag('ol'); + $output .= html_writer::end_tag('div'); + } + + // Next, have there been any modifications to the course structure? + if (!empty($structuralchanges)) { + $content = true; + $output .= $this->heading(get_string("courseupdates").':', 3); + foreach ($structuralchanges as $changeinfo => $change) { + $output .= $this->structural_change($change); + } + } + + // Now display new things from each module + foreach ($modulesrecentactivity as $modname => $moduleactivity) { + $content = true; + $output .= $moduleactivity; + } + + if (! $content) { + $output .= html_writer::tag('p', get_string('nothingnew'), array('class' => 'message')); + } + return $output; + } + + /** + * Renders HTML for one change in course structure + * + * @see block_recent_activity::get_structural_changes() + * @param array $change array containing attributes + * 'action' - one of: 'add mod', 'update mod', 'delete mod' + * 'module' - instance of cm_info (for 'delete mod' it is an object with attributes modname and modfullname) + * @return string + */ + protected function structural_change($change) { + $cm = $change['module']; + switch ($change['action']) { + case 'delete mod': + $text = get_string('deletedactivity', 'moodle', $cm->modfullname); + break; + case 'add mod': + $text = get_string('added', 'moodle', $cm->modfullname). '<br />'. + html_writer::link($cm->url, format_string($cm->name, true)); + break; + case 'update mod': + $text = get_string('updated', 'moodle', $cm->modfullname). '<br />'. + html_writer::link($cm->url, format_string($cm->name, true)); + break; + default: + return ''; + } + return html_writer::tag('p', $text, array('class' => 'activity')); + } +} diff --git a/recent_activity/styles.css b/recent_activity/styles.css new file mode 100644 index 0000000..3d1d9e3 --- /dev/null +++ b/recent_activity/styles.css @@ -0,0 +1,12 @@ +.block_recent_activity .activitydate, +.block_recent_activity .activityhead { + text-align: center; +} + +.block_recent_activity .unlist li { + margin-bottom: 1em; +} + +.block_recent_activity li .head .date { + float: right; +} diff --git a/recent_activity/tests/behat/structural_changes.feature b/recent_activity/tests/behat/structural_changes.feature new file mode 100644 index 0000000..62dc490 --- /dev/null +++ b/recent_activity/tests/behat/structural_changes.feature @@ -0,0 +1,215 @@ +@block @block_recent_activity +Feature: View structural changes in recent activity block + In order to know when activities were changed + As a user + In need to see the structural changes in recent activity block + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + | assistant1 | Terry2 | Teacher2 | teacher2@example.com | + | student1 | Sam1 | Student1 | student1@example.com | + | student2 | Sam2 | Student2 | student2@example.com | + | student3 | Sam3 | Student3 | student3@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | assistant1 | C1 | teacher | + | 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 "groupings" exist: + | name | course | idnumber | + | Grouping 1 | C1 | GG1 | + | Grouping 2 | C1 | GG2 | + | Grouping 3 | C1 | GG3 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G2 | + | student3 | G1 | + | student3 | G2 | + | assistant1 | G1 | + And the following "grouping groups" exist: + | grouping | group | + | GG1 | G1 | + | GG2 | G2 | + | GG3 | G1 | + | GG3 | G2 | + + Scenario: Check that Added module information is displayed respecting view capability + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Recent activity" block + When I add a "Forum" to section "1" and I fill the form with: + | name | ForumVisibleGroups | + | Description | No description | + | groupmode | Visible groups | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumSeparateGroups | + | Description | No description | + | groupmode | Separate groups | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumHidden | + | Description | No description | + | Availability | 0 | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumNoGroups | + | Description | No description | + | groupmode | No groups | + And I add a "Forum" to section "2" and I fill the form with: + | name | ForumVisibleGroupsG1 | + | Description | No description | + | groupmode | Visible groups | + | Grouping | Grouping 1 | + | Access restrictions | Grouping: Grouping 1 | + And I add a "Forum" to section "2" and I fill the form with: + | name | ForumSeparateGroupsG1 | + | Description | No description | + | groupmode | Separate groups | + | Grouping | Grouping 1 | + | Access restrictions | Grouping: Grouping 1 | + And I add a "Forum" to section "3" and I fill the form with: + | name | ForumVisibleGroupsG2 | + | Description | No description | + | groupmode | Visible groups | + | Grouping | Grouping 2 | + | Access restrictions | Grouping: Grouping 2 | + And I add a "Forum" to section "3" and I fill the form with: + | name | ForumSeparateGroupsG2 | + | Description | No description | + | groupmode | Separate groups | + | Grouping | Grouping 2 | + | Access restrictions | Grouping: Grouping 2 | + Then I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should not see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should not see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should not see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should not see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + # Teachers have capability to see all groups and hidden activities + And I log in as "assistant1" + And I am on "Course 1" course homepage + And I should see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + + Scenario: Updates and deletes in recent activity block + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Recent activity" block + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumNew | + | Description | No description | + Then I should see "Added Forum" in the "Recent activity" "block" + And I should see "ForumNew" in the "Recent activity" "block" + And I log out + And I wait "1" seconds + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Added Forum" in the "Recent activity" "block" + And I should see "ForumNew" in the "Recent activity" "block" + And I log out + # Update forum as a teacher after a second to ensure we have a new timestamp for recent activity. + And I wait "1" seconds + # Update forum as a teacher + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "ForumNew" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | name | ForumUpdated | + And I press "Save and return to course" + And I log out + And I wait "1" seconds + # Student 1 already saw that forum was created, now he can see that forum was updated + And I log in as "student1" + And I am on "Course 1" course homepage + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should see "Updated Forum" in the "Recent activity" "block" + And I should see "ForumUpdated" in the "Recent activity" "block" + And I log out + And I wait "1" seconds + # Student 2 has bigger interval and he can see one entry that forum was created but with the new name + And I log in as "student2" + And I am on "Course 1" course homepage + And I should see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should see "ForumUpdated" in the "Recent activity" "block" + And I log out + And I wait "1" seconds + # Delete forum as a teacher + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I delete "ForumUpdated" activity + And I run all adhoc tasks + And I log out + And I wait "1" seconds + # Students 1 and 2 see that forum was deleted + And I log in as "student1" + And I am on "Course 1" course homepage + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should not see "ForumUpdated" in the "Recent activity" "block" + And I should see "Deleted Forum" in the "Recent activity" "block" + And I log out + And I wait "1" seconds + # Student 3 never knew that forum was created, so he does not see anything + And I log in as "student3" + And I am on "Course 1" course homepage + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should not see "ForumUpdated" in the "Recent activity" "block" + And I should not see "Deleted Forum" in the "Recent activity" "block" + And I log out diff --git a/recent_activity/version.php b/recent_activity/version.php new file mode 100644 index 0000000..1943157 --- /dev/null +++ b/recent_activity/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_recent_activity + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_recent_activity'; // Full name of the plugin (used for diagnostics) +$plugin->cron = 24*3600; // Cron interval 1 day. \ No newline at end of file diff --git a/rss_client/backup/moodle1/lib.php b/rss_client/backup/moodle1/lib.php new file mode 100644 index 0000000..b87e0a3 --- /dev/null +++ b/rss_client/backup/moodle1/lib.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/>. + +/** + * Provides support for the conversion of moodle1 backup to the moodle2 format + * + * @package block_rss_client + * @copyright 2012 Paul Nicholls + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Block conversion handler for rss_client + */ +class moodle1_block_rss_client_handler extends moodle1_block_handler { + public function process_block(array $data) { + parent::process_block($data); + $instanceid = $data['id']; + $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']); + + // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml + // for the restore process to find + $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml"); + $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client')); + $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid)); + $this->xmlwriter->full_tag('feeds', ''); + $this->xmlwriter->end_tag('rss_client'); + $this->xmlwriter->end_tag('block'); + $this->close_xml_writer(); + + return $data; + } +} diff --git a/rss_client/backup/moodle2/backup_rss_client_block_task.class.php b/rss_client/backup/moodle2/backup_rss_client_block_task.class.php new file mode 100644 index 0000000..f3500c0 --- /dev/null +++ b/rss_client/backup/moodle2/backup_rss_client_block_task.class.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/>. + +/** + * @package block_rss_client + * @subpackage backup-moodle2 + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot . '/blocks/rss_client/backup/moodle2/backup_rss_client_stepslib.php'); // We have structure steps + +/** + * Specialised backup task for the rss_client block + * (has own DB structures to backup) + * + * TODO: Finish phpdocs + */ +class backup_rss_client_block_task extends backup_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + // rss_client has one structure step + $this->add_step(new backup_rss_client_block_structure_step('rss_client_structure', 'rss_client.xml')); + } + + public function get_fileareas() { + return array(); // No associated fileareas + } + + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata + } + + static public function encode_content_links($content) { + return $content; // No special encoding of links + } +} + diff --git a/rss_client/backup/moodle2/backup_rss_client_stepslib.php b/rss_client/backup/moodle2/backup_rss_client_stepslib.php new file mode 100644 index 0000000..437d708 --- /dev/null +++ b/rss_client/backup/moodle2/backup_rss_client_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 block_rss_client + * @subpackage backup-moodle2 + * @copyright 2003 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 wll be used by the backup_rss_client_block_task + */ + +/** + * Define the complete forum structure for backup, with file and id annotations + */ +class backup_rss_client_block_structure_step extends backup_block_structure_step { + + protected function define_structure() { + global $DB; + + // Get the block + $block = $DB->get_record('block_instances', array('id' => $this->task->get_blockid())); + // Extract configdata + $config = unserialize(base64_decode($block->configdata)); + // Get array of used rss feeds + if (!empty($config->rssid)) { + $feedids = $config->rssid; + // Get the IN corresponding query + list($in_sql, $in_params) = $DB->get_in_or_equal($feedids); + // Define all the in_params as sqlparams + foreach ($in_params as $key => $value) { + $in_params[$key] = backup_helper::is_sqlparam($value); + } + } + + // Define each element separated + + $rss_client = new backup_nested_element('rss_client', array('id'), null); + + $feeds = new backup_nested_element('feeds'); + + $feed = new backup_nested_element('feed', array('id'), array( + 'title', 'preferredtitle', 'description', 'shared', + 'url')); + + // Build the tree + + $rss_client->add_child($feeds); + $feeds->add_child($feed); + + // Define sources + + $rss_client->set_source_array(array((object)array('id' => $this->task->get_blockid()))); + + // Only if there are feeds + if (!empty($config->rssid)) { + $feed->set_source_sql(" + SELECT * + FROM {block_rss_client} + WHERE id $in_sql", $in_params); + } + + // Annotations (none) + + // Return the root element (rss_client), wrapped into standard block structure + return $this->prepare_block_structure($rss_client); + } +} diff --git a/rss_client/backup/moodle2/restore_rss_client_block_task.class.php b/rss_client/backup/moodle2/restore_rss_client_block_task.class.php new file mode 100644 index 0000000..cf99891 --- /dev/null +++ b/rss_client/backup/moodle2/restore_rss_client_block_task.class.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/>. + +/** + * @package block_rss_client + * @subpackage backup-moodle2 + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot . '/blocks/rss_client/backup/moodle2/restore_rss_client_stepslib.php'); // We have structure steps + +/** + * Specialised restore task for the rss_client block + * (has own DB structures to backup) + * + * TODO: Finish phpdocs + */ +class restore_rss_client_block_task extends restore_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + // rss_client has one structure step + $this->add_step(new restore_rss_client_block_structure_step('rss_client_structure', 'rss_client.xml')); + } + + public function get_fileareas() { + return array(); // No associated fileareas + } + + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata + } + + static public function define_decode_contents() { + return array(); + } + + static public function define_decode_rules() { + return array(); + } +} + diff --git a/rss_client/backup/moodle2/restore_rss_client_stepslib.php b/rss_client/backup/moodle2/restore_rss_client_stepslib.php new file mode 100644 index 0000000..cdd9783 --- /dev/null +++ b/rss_client/backup/moodle2/restore_rss_client_stepslib.php @@ -0,0 +1,90 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_rss_client + * @subpackage backup-moodle2 + * @copyright 2003 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 wll be used by the restore_rss_client_block_task + */ + +/** + * Define the complete rss_client structure for restore + */ +class restore_rss_client_block_structure_step extends restore_structure_step { + + protected function define_structure() { + + $paths = array(); + + $paths[] = new restore_path_element('block', '/block', true); + $paths[] = new restore_path_element('rss_client', '/block/rss_client'); + $paths[] = new restore_path_element('feed', '/block/rss_client/feeds/feed'); + + return $paths; + } + + public function process_block($data) { + global $DB; + + $data = (object)$data; + $feedsarr = array(); // To accumulate feeds + + // For any reason (non multiple, dupe detected...) block not restored, return + if (!$this->task->get_blockid()) { + return; + } + + // Iterate over all the feed elements, creating them if needed + if (isset($data->rss_client['feeds']['feed'])) { + foreach ($data->rss_client['feeds']['feed'] as $feed) { + $feed = (object)$feed; + // Look if the same feed is available by url and (shared or userid) + $select = 'url = :url AND (shared = 1 OR userid = :userid)'; + $params = array('url' => $feed->url, 'userid' => $this->task->get_userid()); + // The feed already exists, use it + if ($feedid = $DB->get_field_select('block_rss_client', 'id', $select, $params, IGNORE_MULTIPLE)) { + $feedsarr[] = $feedid; + + // The feed doesn't exist, create it + } else { + $feed->userid = $this->task->get_userid(); + $feedid = $DB->insert_record('block_rss_client', $feed); + $feedsarr[] = $feedid; + } + } + } + + // Adjust the serialized configdata->rssid to the created/mapped feeds + // Get the configdata + $configdata = $DB->get_field('block_instances', 'configdata', array('id' => $this->task->get_blockid())); + // Extract configdata + $config = unserialize(base64_decode($configdata)); + if (empty($config)) { + $config = new stdClass(); + } + // Set array of used rss feeds + $config->rssid = $feedsarr; + // Serialize back the configdata + $configdata = base64_encode(serialize($config)); + // Set the configdata back + $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $this->task->get_blockid())); + } +} diff --git a/rss_client/block_rss_client.php b/rss_client/block_rss_client.php new file mode 100644 index 0000000..bc6d7c5 --- /dev/null +++ b/rss_client/block_rss_client.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/>. + +/** + * Contains block_rss_client + * @package block_rss_client + * @copyright Daryl Hawes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + */ + +/** + * A block which displays Remote feeds + * + * @package block_rss_client + * @copyright Daryl Hawes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + */ + + class block_rss_client extends block_base { + /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */ + const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds. + + /** @var bool track whether any of the output feeds have recorded failures */ + private $hasfailedfeeds = false; + + function init() { + $this->title = get_string('pluginname', 'block_rss_client'); + } + + function applicable_formats() { + return array('all' => true, 'tag' => false); // Needs work to make it work on tags MDL-11960 + } + + function specialization() { + // After the block has been loaded we customize the block's title display + if (!empty($this->config) && !empty($this->config->title)) { + // There is a customized block title, display it + $this->title = $this->config->title; + } else { + // No customized block title, use localized remote news feed string + $this->title = get_string('remotenewsfeed', 'block_rss_client'); + } + } + + /** + * Gets the footer, which is the channel link of the last feed in our list of feeds + * + * @param array $feedrecords The feed records from the database. + * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed. + */ + protected function get_footer($feedrecords) { + global $PAGE; + $footer = null; + + if ($this->config->block_rss_client_show_channel_link) { + global $CFG; + require_once($CFG->libdir.'/simplepie/moodle_simplepie.php'); + + $feedrecord = array_pop($feedrecords); + $feed = new moodle_simplepie($feedrecord->url); + $channellink = new moodle_url($feed->get_link()); + + if (!empty($channellink)) { + $footer = new block_rss_client\output\footer($channellink); + } + } + + if ($this->hasfailedfeeds) { + if (has_any_capability(['block/rss_client:manageownfeeds', 'block/rss_client:manageanyfeeds'], $this->context)) { + if ($footer === null) { + $footer = new block_rss_client\output\footer(); + } + $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php', ['courseid' => $PAGE->course->id]); + $footer->set_failed($manageurl); + } + } + + return $footer; + } + + function get_content() { + global $CFG, $DB; + + if ($this->content !== NULL) { + return $this->content; + } + + // initalise block content object + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + if (!isset($this->config)) { + // The block has yet to be configured - just display configure message in + // the block if user has permission to configure it + + if (has_capability('block/rss_client:manageanyfeeds', $this->context)) { + $this->content->text = get_string('feedsconfigurenewinstance2', 'block_rss_client'); + } + + return $this->content; + } + + // How many feed items should we display? + $maxentries = 5; + if ( !empty($this->config->shownumentries) ) { + $maxentries = intval($this->config->shownumentries); + }elseif( isset($CFG->block_rss_client_num_entries) ) { + $maxentries = intval($CFG->block_rss_client_num_entries); + } + + /* --------------------------------- + * Begin Normal Display of Block Content + * --------------------------------- */ + + $renderer = $this->page->get_renderer('block_rss_client'); + $block = new \block_rss_client\output\block(); + + if (!empty($this->config->rssid)) { + list($rssidssql, $params) = $DB->get_in_or_equal($this->config->rssid); + $rssfeeds = $DB->get_records_select('block_rss_client', "id $rssidssql", $params); + + if (!empty($rssfeeds)) { + $showtitle = false; + if (count($rssfeeds) > 1) { + // When many feeds show the title for each feed. + $showtitle = true; + } + + foreach ($rssfeeds as $feed) { + if ($renderablefeed = $this->get_feed($feed, $maxentries, $showtitle)) { + $block->add_feed($renderablefeed); + } + } + + $footer = $this->get_footer($rssfeeds); + } + } + + $this->content->text = $renderer->render_block($block); + if (isset($footer)) { + $this->content->footer = $renderer->render_footer($footer); + } + + return $this->content; + } + + + function instance_allow_multiple() { + return true; + } + + function has_config() { + return true; + } + + function instance_allow_config() { + return true; + } + + /** + * Returns the html of a feed to be displaed in the block + * + * @param mixed feedrecord The feed record from the database + * @param int maxentries The maximum number of entries to be displayed + * @param boolean showtitle Should the feed title be displayed in html + * @return block_rss_client\output\feed|null The renderable feed or null of there is an error + */ + public function get_feed($feedrecord, $maxentries, $showtitle) { + global $CFG; + require_once($CFG->libdir.'/simplepie/moodle_simplepie.php'); + + if ($feedrecord->skipuntil) { + // Last attempt to gather this feed via cron failed - do not try to fetch it now. + $this->hasfailedfeeds = true; + return null; + } + + $simplepiefeed = new moodle_simplepie($feedrecord->url); + + if(isset($CFG->block_rss_client_timeout)){ + $simplepiefeed->set_cache_duration($CFG->block_rss_client_timeout * 60); + } + + if ($simplepiefeed->error()) { + debugging($feedrecord->url .' Failed with code: '.$simplepiefeed->error()); + return null; + } + + if(empty($feedrecord->preferredtitle)){ + // Simplepie does escape HTML entities. + $feedtitle = $this->format_title($simplepiefeed->get_title()); + }else{ + // Moodle custom title does not does escape HTML entities. + $feedtitle = $this->format_title(s($feedrecord->preferredtitle)); + } + + if (empty($this->config->title)){ + //NOTE: this means the 'last feed' displayed wins the block title - but + //this is exiting behaviour.. + $this->title = strip_tags($feedtitle); + } + + $feed = new \block_rss_client\output\feed($feedtitle, $showtitle, $this->config->block_rss_client_show_channel_image); + + if ($simplepieitems = $simplepiefeed->get_items(0, $maxentries)) { + foreach ($simplepieitems as $simplepieitem) { + try { + $item = new \block_rss_client\output\item( + $simplepieitem->get_id(), + new moodle_url($simplepieitem->get_link()), + $simplepieitem->get_title(), + $simplepieitem->get_description(), + new moodle_url($simplepieitem->get_permalink()), + $simplepieitem->get_date('U'), + $this->config->display_description + ); + + $feed->add_item($item); + } catch (moodle_exception $e) { + // If there is an error with the RSS item, we don't + // want to crash the page. Specifically, moodle_url can + // throw an exception of the param is an extremely + // malformed url. + debugging($e->getMessage()); + } + } + } + + // Feed image. + if ($imageurl = $simplepiefeed->get_image_url()) { + try { + $image = new \block_rss_client\output\channel_image( + new moodle_url($imageurl), + $simplepiefeed->get_image_title(), + new moodle_url($simplepiefeed->get_image_link()) + ); + + $feed->set_image($image); + } catch (moodle_exception $e) { + // If there is an error with the RSS image, we don'twant to + // crash the page. Specifically, moodle_url can throw an + // exception if the param is an extremely malformed url. + debugging($e->getMessage()); + } + } + + return $feed; + } + + /** + * Strips a large title to size and adds ... if title too long + * This function does not escape HTML entities, so they have to be escaped + * before being passed here. + * + * @param string title to shorten + * @param int max character length of title + * @return string title shortened if necessary + */ + function format_title($title,$max=64) { + + if (core_text::strlen($title) <= $max) { + return $title; + } else { + return core_text::substr($title, 0, $max - 3) . '...'; + } + } + + /** + * cron - goes through all the feeds. If the feed has a skipuntil value + * that is less than the current time cron will attempt to retrieve it + * with the cache duration set to 0 in order to force the retrieval of + * the item and refresh the cache. + * + * If a feed fails then the skipuntil time of that feed is set to be + * later than the next expected cron time. The amount of time will + * increase each time the fetch fails until the maximum is reached. + * + * If a feed that has been failing is successfully retrieved it will + * go back to being handled as though it had never failed. + * + * CRON should therefor process requests for permanently broken RSS + * feeds infrequently, and temporarily unavailable feeds will be tried + * less often until they become available again. + * + * @return boolean Always returns true + */ + function cron() { + global $CFG, $DB; + require_once($CFG->libdir.'/simplepie/moodle_simplepie.php'); + + // Get the legacy cron time, strangely the cron property of block_base + // does not seem to get set. This means we must retrive it here. + $this->cron = $DB->get_field('block', 'cron', array('name' => 'rss_client')); + + // We are going to measure execution times + $starttime = microtime(); + $starttimesec = time(); + + // Fetch all site feeds. + $rs = $DB->get_recordset('block_rss_client'); + $counter = 0; + mtrace(''); + foreach ($rs as $rec) { + mtrace(' ' . $rec->url . ' ', ''); + + // Skip feed if it failed recently. + if ($starttimesec < $rec->skipuntil) { + mtrace('skipping until ' . userdate($rec->skipuntil)); + continue; + } + + // Fetch the rss feed, using standard simplepie caching + // so feeds will be renewed only if cache has expired + core_php_time_limit::raise(60); + + $feed = new moodle_simplepie(); + // set timeout for longer than normal to be agressive at + // fetching feeds if possible.. + $feed->set_timeout(40); + $feed->set_cache_duration(0); + $feed->set_feed_url($rec->url); + $feed->init(); + + if ($feed->error()) { + // Skip this feed (for an ever-increasing time if it keeps failing). + $rec->skiptime = $this->calculate_skiptime($rec->skiptime); + $rec->skipuntil = time() + $rec->skiptime; + $DB->update_record('block_rss_client', $rec); + mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds."); + } else { + mtrace ('ok'); + // It worked this time, so reset the skiptime. + if ($rec->skiptime > 0) { + $rec->skiptime = 0; + $rec->skipuntil = 0; + $DB->update_record('block_rss_client', $rec); + } + // Only increase the counter when a feed is sucesfully refreshed. + $counter ++; + } + } + $rs->close(); + + // Show times + mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)'); + + return true; + } + + /** + * Calculates a new skip time for a record based on the current skip time. + * + * @param int $currentskip The curreent skip time of a record. + * @return int A new skip time that should be set. + */ + protected function calculate_skiptime($currentskip) { + // The default time to skiptime. + $newskiptime = $this->cron * 1.1; + if ($currentskip > 0) { + // Double the last time. + $newskiptime = $currentskip * 2; + } + if ($newskiptime > self::CLIENT_MAX_SKIPTIME) { + // Do not allow the skip time to increase indefinatly. + $newskiptime = self::CLIENT_MAX_SKIPTIME; + } + return $newskiptime; + } +} diff --git a/rss_client/classes/output/block.php b/rss_client/classes/output/block.php new file mode 100644 index 0000000..7789f0c --- /dev/null +++ b/rss_client/classes/output/block.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 block_rss_client\output\block + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to help display an RSS Feeds block + * + * @package block_rss_client + * @copyright 2016 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block implements \renderable, \templatable { + + /** + * An array of renderable feeds + * + * @var array + */ + protected $feeds; + + /** + * Contruct + * + * @param array $feeds An array of renderable feeds + */ + public function __construct(array $feeds = array()) { + $this->feeds = $feeds; + } + + /** + * Prepare data for use in a template + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(\renderer_base $output) { + $data = array('feeds' => array()); + + foreach ($this->feeds as $feed) { + $data['feeds'][] = $feed->export_for_template($output); + } + + return $data; + } + + /** + * Add a feed + * + * @param \block_rss_client\output\feed $feed + * @return \block_rss_client\output\block + */ + public function add_feed(feed $feed) { + $this->feeds[] = $feed; + + return $this; + } + + /** + * Set the feeds + * + * @param array $feeds + * @return \block_rss_client\output\block + */ + public function set_feeds(array $feeds) { + $this->feeds = $feeds; + + return $this; + } + + /** + * Get feeds + * + * @return array + */ + public function get_feeds() { + return $this->feeds; + } +} diff --git a/rss_client/classes/output/channel_image.php b/rss_client/classes/output/channel_image.php new file mode 100644 index 0000000..af9e22f --- /dev/null +++ b/rss_client/classes/output/channel_image.php @@ -0,0 +1,151 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_rss_client\output\channel_image + * + * @package block_rss_client + * @copyright 2016 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to display RSS channel images + * + * @package block_rss_client + * @copyright 2016 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class channel_image implements \renderable, \templatable { + + /** + * The URL location of the image + * + * @var string + */ + protected $url; + + /** + * The title of the image + * + * @var string + */ + protected $title; + + /** + * The URL of the image link + * + * @var string + */ + protected $link; + + /** + * Contructor + * + * @param \moodle_url $url The URL location of the image + * @param string $title The title of the image + * @param \moodle_url $link The URL of the image link + */ + public function __construct(\moodle_url $url, $title, \moodle_url $link = null) { + $this->url = $url; + $this->title = $title; + $this->link = $link; + } + + /** + * Export this for use in a mustache template context. + * + * @see templatable::export_for_template() + * @param renderer_base $output + * @return array The data for the template + */ + public function export_for_template(\renderer_base $output) { + return array( + 'url' => clean_param($this->url, PARAM_URL), + 'title' => $this->title, + 'link' => clean_param($this->link, PARAM_URL), + ); + } + + /** + * Set the URL + * + * @param \moodle_url $url + * @return \block_rss_client\output\channel_image + */ + public function set_url(\moodle_url $url) { + $this->url = $url; + + return $this; + } + + /** + * Get the URL + * + * @return \moodle_url + */ + public function get_url() { + return $this->url; + } + + /** + * Set the title + * + * @param string $title + * @return \block_rss_client\output\channel_image + */ + public function set_title($title) { + $this->title = $title; + + return $this; + } + + /** + * Get the title + * + * @return string + */ + public function get_title() { + return $this->title; + } + + /** + * Set the link + * + * @param \moodle_url $link + * @return \block_rss_client\output\channel_image + */ + public function set_link($link) { + $this->link = $link; + + return $this; + } + + /** + * Get the link + * + * @return \moodle_url + */ + public function get_link() { + return $this->link; + } +} diff --git a/rss_client/classes/output/feed.php b/rss_client/classes/output/feed.php new file mode 100644 index 0000000..02f7e2d --- /dev/null +++ b/rss_client/classes/output/feed.php @@ -0,0 +1,224 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 block_rss_client\output\feed + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to help display an RSS Feed + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class feed implements \renderable, \templatable { + + /** + * The feed's title + * + * @var string + */ + protected $title = null; + + /** + * An array of renderable feed items + * + * @var array + */ + protected $items = array(); + + /** + * The channel image + * + * @var channel_image + */ + protected $image = null; + + /** + * Whether or not to show the title + * + * @var boolean + */ + protected $showtitle; + + /** + * Whether or not to show the channel image + * + * @var boolean + */ + protected $showimage; + + /** + * Contructor + * + * @param string $title The title of the RSS feed + * @param boolean $showtitle Whether to show the title + * @param boolean $showimage Whether to show the channel image + */ + public function __construct($title, $showtitle = true, $showimage = true) { + $this->title = $title; + $this->showtitle = $showtitle; + $this->showimage = $showimage; + } + + /** + * Export this for use in a mustache template context. + * + * @see templatable::export_for_template() + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $data = array( + 'title' => $this->showtitle ? $this->title : null, + 'image' => null, + 'items' => array(), + ); + + if ($this->showimage && $this->image) { + $data['image'] = $this->image->export_for_template($output); + } + + foreach ($this->items as $item) { + $data['items'][] = $item->export_for_template($output); + } + + return $data; + } + + /** + * Set the feed title + * + * @param string $title + * @return \block_rss_client\output\feed + */ + public function set_title($title) { + $this->title = $title; + + return $this; + } + + /** + * Get the feed title + * + * @return string + */ + public function get_title() { + return $this->title; + } + + /** + * Add an RSS item + * + * @param \block_rss_client\output\item $item + */ + public function add_item(item $item) { + $this->items[] = $item; + + return $this; + } + + /** + * Set the RSS items + * + * @param array $items An array of renderable RSS items + */ + public function set_items(array $items) { + $this->items = $items; + + return $this; + } + + /** + * Get the RSS items + * + * @return array An array of renderable RSS items + */ + public function get_items() { + return $this->items; + } + + /** + * Set the channel image + * + * @param \block_rss_client\output\channel_image $image + */ + public function set_image(channel_image $image) { + $this->image = $image; + } + + /** + * Get the channel image + * + * @return channel_image + */ + public function get_image() { + return $this->image; + } + + /** + * Set showtitle + * + * @param boolean $showtitle + * @return \block_rss_client\output\feed + */ + public function set_showtitle($showtitle) { + $this->showtitle = boolval($showtitle); + + return $this; + } + + /** + * Get showtitle + * + * @return boolean + */ + public function get_showtitle() { + return $this->showtitle; + } + + /** + * Set showimage + * + * @param boolean $showimage + * @return \block_rss_client\output\feed + */ + public function set_showimage($showimage) { + $this->showimage = boolval($showimage); + + return $this; + } + + /** + * Get showimage + * + * @return boolean + */ + public function get_showimage() { + return $this->showimage; + } +} diff --git a/rss_client/classes/output/footer.php b/rss_client/classes/output/footer.php new file mode 100644 index 0000000..c864df3 --- /dev/null +++ b/rss_client/classes/output/footer.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/>. + +/** + * Contains class block_rss_client\output\footer + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to help display an RSS Block footer + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class footer implements \renderable, \templatable { + + /** + * The link provided in the RSS channel + * + * @var \moodle_url|null + */ + protected $channelurl; + + /** + * Link to manage feeds, only provided if a feed has failed. + * + * @var \moodle_url|null + */ + protected $manageurl = null; + + /** + * Constructor + * + * @param \moodle_url $channelurl (optional) The link provided in the RSS channel + */ + public function __construct($channelurl = null) { + $this->channelurl = $channelurl; + } + + /** + * Set the channel url + * + * @param \moodle_url $channelurl + * @return \block_rss_client\output\footer + */ + public function set_channelurl(\moodle_url $channelurl) { + $this->channelurl = $channelurl; + + return $this; + } + + /** + * Record the fact that there is at least one failed feed (and the URL for viewing + * these failed feeds). + * + * @param \moodle_url $manageurl the URL to link to for more information + */ + public function set_failed(\moodle_url $manageurl) { + $this->manageurl = $manageurl; + } + + /** + * Get the channel url + * + * @return \moodle_url + */ + public function get_channelurl() { + return $this->channelurl; + } + + /** + * Export context for use in mustache templates + * + * @see templatable::export_for_template() + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $data = new \stdClass(); + $data->channellink = clean_param($this->channelurl, PARAM_URL); + if ($this->manageurl) { + $data->hasfailedfeeds = true; + $data->manageurl = clean_param($this->manageurl, PARAM_URL); + } + + return $data; + } +} diff --git a/rss_client/classes/output/item.php b/rss_client/classes/output/item.php new file mode 100644 index 0000000..e988304 --- /dev/null +++ b/rss_client/classes/output/item.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/>. + +/** + * Contains class block_rss_client\output\feed + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to help display an RSS Item + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class item implements \renderable, \templatable { + + /** + * The unique id of the item + * + * @var string + */ + protected $id; + + /** + * The link to the item + * + * @var \moodle_url + */ + protected $link; + + /** + * The title of the item + * + * @var string + */ + protected $title; + + /** + * The description of the item + * + * @var string + */ + protected $description; + + /** + * The item's permalink + * + * @var \moodle_url + */ + protected $permalink; + + /** + * The publish date of the item in Unix timestamp format + * + * @var int + */ + protected $timestamp; + + /** + * Whether or not to show the item's description + * + * @var string + */ + protected $showdescription; + + /** + * Contructor + * + * @param string $id The id of the RSS item + * @param \moodle_url $link The URL of the RSS item + * @param string $title The title pf the RSS item + * @param string $description The description of the RSS item + * @param \moodle_url $permalink The permalink of the RSS item + * @param int $timestamp The Unix timestamp that represents the published date + * @param boolean $showdescription Whether or not to show the description + */ + public function __construct($id, \moodle_url $link, $title, $description, \moodle_url $permalink, $timestamp, + $showdescription = true) { + $this->id = $id; + $this->link = $link; + $this->title = $title; + $this->description = $description; + $this->permalink = $permalink; + $this->timestamp = $timestamp; + $this->showdescription = $showdescription; + } + + /** + * Export context for use in mustache templates + * + * @see templatable::export_for_template() + * @param renderer_base $output + * @return array + */ + public function export_for_template(\renderer_base $output) { + $data = array( + 'id' => $this->id, + 'permalink' => clean_param($this->permalink, PARAM_URL), + 'datepublished' => $output->format_published_date($this->timestamp), + 'link' => clean_param($this->link, PARAM_URL), + ); + + // If the item does not have a title, create one from the description. + $title = $this->title; + if (!$title) { + $title = strip_tags($this->description); + $title = \core_text::substr($title, 0, 20) . '...'; + } + + // Allow the renderer to format the title and description. + $data['title'] = $output->format_title($title); + $data['description'] = $this->showdescription ? $output->format_description($this->description) : null; + + return $data; + } + + /** + * Set id + * + * @param string $id + * @return \block_rss_client\output\item + */ + public function set_id($id) { + $this->id = $id; + + return $this; + } + + /** + * Get id + * + * @return string + */ + public function get_id() { + return $this->id; + } + + /** + * Set link + * + * @param \moodle_url $link + * @return \block_rss_client\output\item + */ + public function set_link(\moodle_url $link) { + $this->link = $link; + + return $this; + } + + /** + * Get link + * + * @return \moodle_url + */ + public function get_link() { + return $this->link; + } + + /** + * Set title + * + * @param string $title + * @return \block_rss_client\output\item + */ + public function set_title($title) { + $this->title = $title; + + return $this; + } + + /** + * Get title + * + * @return string + */ + public function get_title() { + return $this->title; + } + + /** + * Set description + * + * @param string $description + * @return \block_rss_client\output\item + */ + public function set_description($description) { + $this->description = $description; + + return $this; + } + + /** + * Get description + * + * @return string + */ + public function get_description() { + return $this->description; + } + + /** + * Set permalink + * + * @param string $permalink + * @return \block_rss_client\output\item + */ + public function set_permalink($permalink) { + $this->permalink = $permalink; + + return $this; + } + + /** + * Get permalink + * + * @return string + */ + public function get_permalink() { + return $this->permalink; + } + + /** + * Set timestamp + * + * @param int $timestamp + * @return \block_rss_client\output\item + */ + public function set_timestamp($timestamp) { + $this->timestamp = $timestamp; + + return $this; + } + + /** + * Get timestamp + * + * @return string + */ + public function get_timestamp() { + return $this->timestamp; + } + + /** + * Set showdescription + * + * @param boolean $showdescription + * @return \block_rss_client\output\item + */ + public function set_showdescription($showdescription) { + $this->showdescription = boolval($showdescription); + + return $this; + } + + /** + * Get showdescription + * + * @return boolean + */ + public function get_showdescription() { + return $this->showdescription; + } +} diff --git a/rss_client/classes/output/renderer.php b/rss_client/classes/output/renderer.php new file mode 100644 index 0000000..7a03280 --- /dev/null +++ b/rss_client/classes/output/renderer.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/>. + +/** + * Contains class block_rss_client\output\block_renderer_html + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Renderer for RSS Client block + * + * @package block_rss_client + * @copyright 2015 Howard County Public School System + * @author Brendan Anderson <brendan_anderson@hcpss.org> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Render an RSS Item + * + * @param templatable $item + * @return string|boolean + */ + public function render_item(\templatable $item) { + $data = $item->export_for_template($this); + + return $this->render_from_template('block_rss_client/item', $data); + } + + /** + * Render an RSS Feed + * + * @param templatable $feed + * @return string|boolean + */ + public function render_feed(\templatable $feed) { + $data = $feed->export_for_template($this); + + return $this->render_from_template('block_rss_client/feed', $data); + } + + /** + * Render an RSS feeds block + * + * @param \templatable $block + * @return string|boolean + */ + public function render_block(\templatable $block) { + $data = $block->export_for_template($this); + + return $this->render_from_template('block_rss_client/block', $data); + } + + /** + * Render the block footer + * + * @param templatable $footer + * @return string|boolean + */ + public function render_footer(\templatable $footer) { + $data = $footer->export_for_template($this); + + return $this->render_from_template('block_rss_client/footer', $data); + } + + /** + * Format a timestamp to use as a published date + * + * @param int $timestamp Unix timestamp + * @return string + */ + public function format_published_date($timestamp) { + return strftime(get_string('strftimerecentfull', 'langconfig'), $timestamp); + return date('j F Y, g:i a', $timestamp); + } + + /** + * Format an RSS item title + * + * @param string $title + * @return string + */ + public function format_title($title) { + return break_up_long_words($title, 30); + } + + /** + * Format an RSS item description + * + * @param string $description + * @return string + */ + public function format_description($description) { + $description = format_text($description, FORMAT_HTML, array('para' => false)); + $description = break_up_long_words($description, 30); + + return $description; + } +} diff --git a/rss_client/classes/privacy/provider.php b/rss_client/classes/privacy/provider.php new file mode 100644 index 0000000..c1ea54f --- /dev/null +++ b/rss_client/classes/privacy/provider.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/>. +/** + * Privacy class for requesting user data. + * + * @package block_rss_client + * @copyright 2018 Mihail Geshoski <mihail@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_rss_client\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +/** + * Privacy class for requesting user data. + * + * @package block_rss_client + * @copyright 2018 Mihail Geshoski <mihail@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { + + /** + * 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('block_rss_client', [ + 'userid' => 'privacy:metadata:block_rss_client:userid', + 'title' => 'privacy:metadata:block_rss_client:title', + 'preferredtitle' => 'privacy:metadata:block_rss_client:preferredtitle', + 'description' => 'privacy:metadata:block_rss_client:description', + 'shared' => 'privacy:metadata:block_rss_client:shared', + 'url' => 'privacy:metadata:block_rss_client:url', + 'skiptime' => 'privacy:metadata:block_rss_client:skiptime', + 'skipuntil' => 'privacy:metadata:block_rss_client:skipuntil', + ], 'privacy:metadata:block_rss_client:tableexplanation'); + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $sql = "SELECT ctx.id + FROM {block_rss_client} brc + JOIN {user} u + ON brc.userid = u.id + JOIN {context} ctx + ON ctx.instanceid = u.id + AND ctx.contextlevel = :contextlevel + WHERE brc.userid = :userid"; + + $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; + + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * 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) { + $rssdata = []; + $results = static::get_records($contextlist->get_user()->id); + foreach ($results as $result) { + $rssdata[] = (object) [ + 'title' => $result->title, + 'preferredtitle' => $result->preferredtitle, + 'description' => $result->description, + 'shared' => \core_privacy\local\request\transform::yesno($result->shared), + 'url' => $result->url + ]; + } + if (!empty($rssdata)) { + $data = (object) [ + 'feeds' => $rssdata, + ]; + \core_privacy\local\request\writer::with_context($contextlist->current())->export_data([ + get_string('pluginname', 'block_rss_client')], $data); + } + } + + /** + * Delete all use data which matches the specified deletion_criteria. + * + * @param context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + if ($context instanceof \context_user) { + static::delete_data($context->instanceid); + } + } + + /** + * 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) { + static::delete_data($contextlist->get_user()->id); + } + + /** + * Delete data related to a userid. + * + * @param int $userid The user ID + */ + protected static function delete_data($userid) { + global $DB; + + $DB->delete_records('block_rss_client', ['userid' => $userid]); + } + + /** + * Get records related to this plugin and user. + * + * @param int $userid The user ID + * @return array An array of records. + */ + protected static function get_records($userid) { + global $DB; + + return $DB->get_records('block_rss_client', ['userid' => $userid]); + } +} diff --git a/rss_client/db/access.php b/rss_client/db/access.php new file mode 100644 index 0000000..3789a58 --- /dev/null +++ b/rss_client/db/access.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/>. + +/** + * RSS client block caps. + * + * @package block_rss_client + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/rss_client:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/rss_client:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + + 'block/rss_client:manageownfeeds' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'block/rss_client:manageanyfeeds' => array( + + 'riskbitmask' => RISK_SPAM, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ) + ) + +); + + diff --git a/rss_client/db/install.xml b/rss_client/db/install.xml new file mode 100644 index 0000000..7a7e9cb --- /dev/null +++ b/rss_client/db/install.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="blocks/rss_client/db" VERSION="20150717" COMMENT="XMLDB file for Moodle rss_client block" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="block_rss_client" COMMENT="Remote news feed information. Contains the news feed id, the userid of the user who added the feed, the title of the feed itself and a description of the feed contents along with the url used to access the remote feed. Preferredtitle is a field for future use - intended to allow for custom titles rather than those found in the feed"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="title" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="preferredtitle" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="shared" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="skiptime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="How many seconds skip this feed for (increases every time it fails, resets to 0 when it succeeds)"/> + <FIELD NAME="skipuntil" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Do not query this RSS feed again until this time"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" /> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> diff --git a/rss_client/db/upgrade.php b/rss_client/db/upgrade.php new file mode 100644 index 0000000..d968dda --- /dev/null +++ b/rss_client/db/upgrade.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/>. + +/** + * Database upgrades for the RSS block. + * + * @package block_rss_client + * @copyright 2014 Davo Smith + * @author Neill Magill <neill.magill@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade the block_rss_client database. + * + * @param int $oldversion The version number of the plugin that was installed. + * @return boolean + */ +function xmldb_block_rss_client_upgrade($oldversion) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/rss_client/edit_form.php b/rss_client/edit_form.php new file mode 100644 index 0000000..0c70060 --- /dev/null +++ b/rss_client/edit_form.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/>. + +/** + * Form for editing RSS client block instances. + * + * @package block_rss_client + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing RSS client block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_rss_client_edit_form extends block_edit_form { + protected function specific_definition($mform) { + global $CFG, $DB, $USER; + + // Fields for editing block contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('selectyesno', 'config_display_description', get_string('displaydescriptionlabel', 'block_rss_client')); + $mform->setDefault('config_display_description', 0); + + $mform->addElement('text', 'config_shownumentries', get_string('shownumentrieslabel', 'block_rss_client'), array('size' => 5)); + $mform->setType('config_shownumentries', PARAM_INT); + $mform->addRule('config_shownumentries', null, 'numeric', null, 'client'); + if (!empty($CFG->block_rss_client_num_entries)) { + $mform->setDefault('config_shownumentries', $CFG->block_rss_client_num_entries); + } else { + $mform->setDefault('config_shownumentries', 5); + } + + $insql = ''; + $params = array('userid' => $USER->id); + $rssconfig = unserialize(base64_decode($this->block->instance->configdata)); + if ($rssconfig && !empty($rssconfig->rssid)) { + list($insql, $inparams) = $DB->get_in_or_equal($rssconfig->rssid, SQL_PARAMS_NAMED); + $insql = "OR id $insql "; + $params += $inparams; + } + + $titlesql = "CASE WHEN {$DB->sql_isempty('block_rss_client','preferredtitle', false, false)} + THEN {$DB->sql_compare_text('title', 64)} ELSE preferredtitle END"; + + $rssfeeds = $DB->get_records_sql_menu(" + SELECT id, $titlesql + FROM {block_rss_client} + WHERE userid = :userid OR shared = 1 $insql + ORDER BY $titlesql", + $params); + + if ($rssfeeds) { + $select = $mform->addElement('select', 'config_rssid', get_string('choosefeedlabel', 'block_rss_client'), $rssfeeds); + $select->setMultiple(true); + + } else { + $mform->addElement('static', 'config_rssid_no_feeds', get_string('choosefeedlabel', 'block_rss_client'), + get_string('nofeeds', 'block_rss_client')); + } + + if (has_any_capability(array('block/rss_client:manageanyfeeds', 'block/rss_client:manageownfeeds'), $this->block->context)) { + $mform->addElement('static', 'nofeedmessage', '', + '<a href="' . $CFG->wwwroot . '/blocks/rss_client/managefeeds.php?courseid=' . $this->page->course->id . '">' . + get_string('feedsaddedit', 'block_rss_client') . '</a>'); + } + + $mform->addElement('text', 'config_title', get_string('uploadlabel')); + $mform->setType('config_title', PARAM_NOTAGS); + + $mform->addElement('selectyesno', 'config_block_rss_client_show_channel_link', get_string('clientshowchannellinklabel', 'block_rss_client')); + $mform->setDefault('config_block_rss_client_show_channel_link', 0); + + $mform->addElement('selectyesno', 'config_block_rss_client_show_channel_image', get_string('clientshowimagelabel', 'block_rss_client')); + $mform->setDefault('config_block_rss_client_show_channel_image', 0); + } +} diff --git a/rss_client/editfeed.php b/rss_client/editfeed.php new file mode 100644 index 0000000..97e1706 --- /dev/null +++ b/rss_client/editfeed.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/>. + +/** + * Script to let a user edit the properties of a particular RSS feed. + * + * @package block_rss_client + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir .'/simplepie/moodle_simplepie.php'); + +class feed_edit_form extends moodleform { + protected $isadding; + protected $caneditshared; + protected $title = ''; + protected $description = ''; + + function __construct($actionurl, $isadding, $caneditshared) { + $this->isadding = $isadding; + $this->caneditshared = $caneditshared; + parent::__construct($actionurl); + } + + function definition() { + $mform =& $this->_form; + + // Then show the fields about where this block appears. + $mform->addElement('header', 'rsseditfeedheader', get_string('feed', 'block_rss_client')); + + $mform->addElement('text', 'url', get_string('feedurl', 'block_rss_client'), array('size' => 60)); + $mform->setType('url', PARAM_URL); + $mform->addRule('url', null, 'required'); + + $mform->addElement('checkbox', 'autodiscovery', get_string('enableautodiscovery', 'block_rss_client')); + $mform->setDefault('autodiscovery', 1); + $mform->setAdvanced('autodiscovery'); + $mform->addHelpButton('autodiscovery', 'enableautodiscovery', 'block_rss_client'); + + $mform->addElement('text', 'preferredtitle', get_string('customtitlelabel', 'block_rss_client'), array('size' => 60)); + $mform->setType('preferredtitle', PARAM_NOTAGS); + + if ($this->caneditshared) { + $mform->addElement('selectyesno', 'shared', get_string('sharedfeed', 'block_rss_client')); + $mform->setDefault('shared', 0); + } + + $submitlabal = null; // Default + if ($this->isadding) { + $submitlabal = get_string('addnewfeed', 'block_rss_client'); + } + $this->add_action_buttons(true, $submitlabal); + } + + function definition_after_data(){ + $mform =& $this->_form; + + if($mform->getElementValue('autodiscovery')){ + $mform->applyFilter('url', 'feed_edit_form::autodiscover_feed_url'); + } + } + + function validation($data, $files) { + $errors = parent::validation($data, $files); + + $rss = new moodle_simplepie(); + // set timeout for longer than normal to try and grab the feed + $rss->set_timeout(10); + $rss->set_feed_url($data['url']); + $rss->set_autodiscovery_cache_duration(0); + $rss->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE); + $rss->init(); + + if ($rss->error()) { + $errors['url'] = get_string('couldnotfindloadrssfeed', 'block_rss_client'); + } else { + $this->title = $rss->get_title(); + $this->description = $rss->get_description(); + } + + return $errors; + } + + function get_data() { + $data = parent::get_data(); + if ($data) { + $data->title = ''; + $data->description = ''; + + if($this->title){ + $data->title = $this->title; + } + + if($this->description){ + $data->description = $this->description; + } + } + return $data; + } + + /** + * Autodiscovers a feed url from a given url, to be used by the formslibs + * filter function + * + * Uses simplepie with autodiscovery set to maximum level to try and find + * a feed to subscribe to. + * See: http://simplepie.org/wiki/reference/simplepie/set_autodiscovery_level + * + * @param string URL to autodiscover a url + * @return string URL of feed or original url if none found + */ + public static function autodiscover_feed_url($url){ + $rss = new moodle_simplepie(); + $rss->set_feed_url($url); + $rss->set_autodiscovery_level(SIMPLEPIE_LOCATOR_ALL); + // When autodiscovering an RSS feed, simplepie will try lots of + // rss links on a page, so set the timeout high + $rss->set_timeout(20); + $rss->init(); + + if($rss->error()){ + return $url; + } + + // return URL without quoting.. + $discoveredurl = new moodle_url($rss->subscribe_url()); + return $discoveredurl->out(false); + } +} + +$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); +$courseid = optional_param('courseid', 0, PARAM_INT); +$rssid = optional_param('rssid', 0, PARAM_INT); // 0 mean create new. + +if ($courseid == SITEID) { + $courseid = 0; +} +if ($courseid) { + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $PAGE->set_course($course); + $context = $PAGE->context; +} else { + $context = context_system::instance(); + $PAGE->set_context($context); +} + +$managesharedfeeds = has_capability('block/rss_client:manageanyfeeds', $context); +if (!$managesharedfeeds) { + require_capability('block/rss_client:manageownfeeds', $context); +} + +$urlparams = array('rssid' => $rssid); +if ($courseid) { + $urlparams['courseid'] = $courseid; +} +if ($returnurl) { + $urlparams['returnurl'] = $returnurl; +} +$managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams); + +$PAGE->set_url('/blocks/rss_client/editfeed.php', $urlparams); +$PAGE->set_pagelayout('admin'); + +if ($rssid) { + $isadding = false; + $rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST); +} else { + $isadding = true; + $rssrecord = new stdClass; +} + +$mform = new feed_edit_form($PAGE->url, $isadding, $managesharedfeeds); +$mform->set_data($rssrecord); + +if ($mform->is_cancelled()) { + redirect($managefeeds); + +} else if ($data = $mform->get_data()) { + $data->userid = $USER->id; + if (!$managesharedfeeds) { + $data->shared = 0; + } + + if ($isadding) { + $DB->insert_record('block_rss_client', $data); + } else { + $data->id = $rssid; + $DB->update_record('block_rss_client', $data); + } + + redirect($managefeeds); + +} else { + if ($isadding) { + $strtitle = get_string('addnewfeed', 'block_rss_client'); + } else { + $strtitle = get_string('editafeed', 'block_rss_client'); + } + + $PAGE->set_title($strtitle); + $PAGE->set_heading($strtitle); + + $PAGE->navbar->add(get_string('blocks')); + $PAGE->navbar->add(get_string('pluginname', 'block_rss_client')); + $PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds ); + $PAGE->navbar->add($strtitle); + + echo $OUTPUT->header(); + echo $OUTPUT->heading($strtitle, 2); + + $mform->display(); + + echo $OUTPUT->footer(); +} + diff --git a/rss_client/lang/en/block_rss_client.php b/rss_client/lang/en/block_rss_client.php new file mode 100644 index 0000000..3045074 --- /dev/null +++ b/rss_client/lang/en/block_rss_client.php @@ -0,0 +1,90 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_rss_client', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_rss_client + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['addfeed'] = 'Add a news feed URL:'; +$string['addheadlineblock'] = 'Add RSS headline block'; +$string['addnew'] = 'Add new'; +$string['addnewfeed'] = 'Add a new feed'; +$string['cannotmakemodification'] = 'You are not allowed to make modifications to this RSS feed at this time.'; +$string['clientchannellink'] = 'Source site...'; +$string['clientnumentries'] = 'The default number of entries to show per feed.'; +$string['clientshowchannellinklabel'] = 'Should a link to the original site (channel link) be displayed? (Note that if no feed link is supplied in the news feed then no link will be shown) :'; +$string['clientshowimagelabel'] = 'Show channel image if available :'; +$string['configblock'] = 'Configure this block'; +$string['couldnotfindfeed'] = 'Could not find feed with id'; +$string['couldnotfindloadrssfeed'] = 'Could not find or load the RSS feed.'; +$string['customtitlelabel'] = 'Custom title (leave blank to use title supplied by feed):'; +$string['deletefeedconfirm'] = 'Are you sure you want to delete this feed?'; +$string['disabledrssfeeds'] = 'RSS feeds are disabled'; +$string['displaydescriptionlabel'] = 'Display each link\'s description?'; +$string['editafeed'] = 'Edit a feed'; +$string['editfeeds'] = 'Edit, subscribe or unsubscribe from RSS/Atom news feeds'; +$string['editnewsfeeds'] = 'Edit news feeds'; +$string['editrssblock'] = 'Edit RSS headline block'; +$string['enableautodiscovery'] = 'Enable auto-discovery of feeds?'; +$string['enableautodiscovery_help'] = 'If enabled, feeds on web pages are found automatically. For example, if http://docs.moodle.org is entered, then http://docs.moodle.org/en/index.php?title=Special:RecentChanges&feed=rss would be found.'; +$string['failedfeed'] = 'Feed failed to download - will retry after {$a}'; +$string['failedfeeds'] = 'One or more RSS feeds have failed'; +$string['feed'] = 'Feed'; +$string['feedadded'] = 'News feed added'; +$string['feeddeleted'] = 'News feed deleted'; +$string['feeds'] = 'News feeds'; +$string['feedsaddedit'] = 'Add/edit feeds'; +$string['feedsconfigurenewinstance'] = 'Click here to configure this block to display RSS feeds.'; +$string['feedsconfigurenewinstance2'] = 'Click the edit icon above to configure this block to display RSS feeds.'; +$string['feedupdated'] = 'News feed updated'; +$string['feedurl'] = 'Feed URL'; +$string['findmorefeeds'] = 'Find more RSS feeds'; +$string['choosefeedlabel'] = 'Choose the feeds which you would like to make available in this block:'; +$string['managefeeds'] = 'Manage all my feeds'; +$string['nofeeds'] = 'There are no RSS feeds defined for this site.'; +$string['numentries'] = 'Entries per feed'; +$string['pickfeed'] = 'Pick a news feed'; +$string['pluginname'] = 'Remote RSS feeds'; +$string['privacy:metadata:block_rss_client:description'] = 'The description of the RSS feed.'; +$string['privacy:metadata:block_rss_client:preferredtitle'] = 'The preferred (custom) title of the RSS feed.'; +$string['privacy:metadata:block_rss_client:shared'] = 'If the RSS feed is available to all courses.'; +$string['privacy:metadata:block_rss_client:skiptime'] = 'The defined time in seconds that the cron will wait between attempts to retry failing RSS feeds.'; +$string['privacy:metadata:block_rss_client:skipuntil'] = 'The maximum defined time that the cron will attempt to open failing RSS feeds.'; +$string['privacy:metadata:block_rss_client:tableexplanation'] = 'RSS block information is stored here.'; +$string['privacy:metadata:block_rss_client:title'] = 'The title of the RSS feed.'; +$string['privacy:metadata:block_rss_client:url'] = 'The URL of the RSS feed.'; +$string['privacy:metadata:block_rss_client:userid'] = 'The ID of the user that added the RSS feed.'; +$string['remotenewsfeed'] = 'Remote news feed'; +$string['rss_client:addinstance'] = 'Add a new remote RSS feeds block'; +$string['rss_client:createprivatefeeds'] = 'Create private RSS feeds'; +$string['rss_client:createsharedfeeds'] = 'Create shared RSS feeds'; +$string['rss_client:manageanyfeeds'] = 'Manage any RSS feeds'; +$string['rss_client:manageownfeeds'] = 'Manage own RSS feeds'; +$string['rss_client:myaddinstance'] = 'Add a new RSS feeds block to Dashboard'; +$string['seeallfeeds'] = 'See all feeds'; +$string['sharedfeed'] = 'Shared feed'; +$string['shownumentrieslabel'] = 'Max number entries to show per block.'; +$string['submitters'] = 'Who will be allowed to define new RSS feeds? Defined feeds are available for any page on your site.'; +$string['submitters2'] = 'Submitters'; +$string['timeout'] = 'Time in minutes before an RSS feed expires in cache. Note that this time defines the minimum time before expiry; the feed will be refreshed in cache on the next cron execution after expiry. Recommended values are 30 mins or greater.'; +$string['timeoutdesc'] = 'Time in minutes for an RSS feed to live in cache.'; +$string['timeout2'] = 'Timeout'; +$string['updatefeed'] = 'Update a news feed URL:'; +$string['viewfeed'] = 'View feed'; diff --git a/rss_client/managefeeds.php b/rss_client/managefeeds.php new file mode 100644 index 0000000..d216c3c --- /dev/null +++ b/rss_client/managefeeds.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/>. + +/** + * Script to let a user manage their RSS feeds. + * + * @package block_rss_client + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/tablelib.php'); + +require_login(); + +$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); +$courseid = optional_param('courseid', 0, PARAM_INT); +$deleterssid = optional_param('deleterssid', 0, PARAM_INT); + +if ($courseid == SITEID) { + $courseid = 0; +} +if ($courseid) { + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $PAGE->set_course($course); + $context = $PAGE->context; +} else { + $context = context_system::instance(); + $PAGE->set_context($context); +} + +$managesharedfeeds = has_capability('block/rss_client:manageanyfeeds', $context); +if (!$managesharedfeeds) { + require_capability('block/rss_client:manageownfeeds', $context); +} + +$urlparams = array(); +$extraparams = ''; +if ($courseid) { + $urlparams['courseid'] = $courseid; + $extraparams = '&courseid=' . $courseid; +} +if ($returnurl) { + $urlparams['returnurl'] = $returnurl; + $extraparams = '&returnurl=' . $returnurl; +} +$baseurl = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams); +$PAGE->set_url($baseurl); + +// Process any actions +if ($deleterssid && confirm_sesskey()) { + $DB->delete_records('block_rss_client', array('id'=>$deleterssid)); + + redirect($PAGE->url, get_string('feeddeleted', 'block_rss_client')); +} + +// Display the list of feeds. +if ($managesharedfeeds) { + $select = '(userid = ' . $USER->id . ' OR shared = 1)'; +} else { + $select = 'userid = ' . $USER->id; +} +$feeds = $DB->get_records_select('block_rss_client', $select, null, $DB->sql_order_by_text('title')); + +$strmanage = get_string('managefeeds', 'block_rss_client'); + +$PAGE->set_pagelayout('standard'); +$PAGE->set_title($strmanage); +$PAGE->set_heading($strmanage); + +$managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams); +$PAGE->navbar->add(get_string('blocks')); +$PAGE->navbar->add(get_string('pluginname', 'block_rss_client')); +$PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds); +echo $OUTPUT->header(); + +$table = new flexible_table('rss-display-feeds'); + +$table->define_columns(array('feed', 'actions')); +$table->define_headers(array(get_string('feed', 'block_rss_client'), get_string('actions', 'moodle'))); +$table->define_baseurl($baseurl); + +$table->set_attribute('cellspacing', '0'); +$table->set_attribute('id', 'rssfeeds'); +$table->set_attribute('class', 'generaltable generalbox'); +$table->column_class('feed', 'feed'); +$table->column_class('actions', 'actions'); + +$table->setup(); + +foreach($feeds as $feed) { + if (!empty($feed->preferredtitle)) { + $feedtitle = s($feed->preferredtitle); + } else { + $feedtitle = $feed->title; + } + + $viewlink = html_writer::link($CFG->wwwroot .'/blocks/rss_client/viewfeed.php?rssid=' . $feed->id . $extraparams, $feedtitle); + + $feedinfo = '<div class="title">' . $viewlink . '</div>' . + '<div class="url">' . html_writer::link($feed->url, $feed->url) .'</div>' . + '<div class="description">' . $feed->description . '</div>'; + if ($feed->skipuntil) { + $skipuntil = userdate($feed->skipuntil, get_string('strftimedatetime', 'langconfig')); + $skipmsg = get_string('failedfeed', 'block_rss_client', $skipuntil); + $notification = new \core\output\notification($skipmsg, 'error'); + $notification->set_show_closebutton(false); + $feedinfo .= $OUTPUT->render($notification); + } + + $editurl = new moodle_url('/blocks/rss_client/editfeed.php?rssid=' . $feed->id . $extraparams); + $editaction = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('edit'))); + + $deleteurl = new moodle_url('/blocks/rss_client/managefeeds.php?deleterssid=' . $feed->id . '&sesskey=' . sesskey() . $extraparams); + $deleteicon = new pix_icon('t/delete', get_string('delete')); + $deleteaction = $OUTPUT->action_icon($deleteurl, $deleteicon, new confirm_action(get_string('deletefeedconfirm', 'block_rss_client'))); + + $feedicons = $editaction . ' ' . $deleteaction; + + $table->add_data(array($feedinfo, $feedicons)); +} + +$table->print_html(); + +$url = $CFG->wwwroot . '/blocks/rss_client/editfeed.php?' . substr($extraparams, 1); +echo '<div class="actionbuttons">' . $OUTPUT->single_button($url, get_string('addnewfeed', 'block_rss_client'), 'get') . '</div>'; + + +if ($returnurl) { + echo '<div class="backlink">' . html_writer::link($returnurl, get_string('back')) . '</div>'; +} + +echo $OUTPUT->footer(); diff --git a/rss_client/settings.php b/rss_client/settings.php new file mode 100644 index 0000000..cb98280 --- /dev/null +++ b/rss_client/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/>. + +/** + * Settings for the RSS client block. + * + * @package block_rss_client + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtext('block_rss_client_num_entries', get_string('numentries', 'block_rss_client'), + get_string('clientnumentries', 'block_rss_client'), 5, PARAM_INT)); + + $settings->add(new admin_setting_configtext('block_rss_client_timeout', get_string('timeout2', 'block_rss_client'), + get_string('timeout', 'block_rss_client'), 30, PARAM_INT)); + + $link ='<a href="'.$CFG->wwwroot.'/blocks/rss_client/managefeeds.php">'.get_string('feedsaddedit', 'block_rss_client').'</a>'; + $settings->add(new admin_setting_heading('block_rss_addheading', '', $link)); +} \ No newline at end of file diff --git a/rss_client/styles.css b/rss_client/styles.css new file mode 100644 index 0000000..b1afcae --- /dev/null +++ b/rss_client/styles.css @@ -0,0 +1,10 @@ +/* RSS Feeds +-------------------------*/ +.block_rss_client .list li:first-child { + border-top-width: 0; +} + +.block_rss_client .list li { + border-top: 1px solid; + padding: 5px; +} \ No newline at end of file diff --git a/rss_client/templates/block.mustache b/rss_client/templates/block.mustache new file mode 100644 index 0000000..6cc2c71 --- /dev/null +++ b/rss_client/templates/block.mustache @@ -0,0 +1,91 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_rss_client/block + + Template which defines an RSS Feeds block + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * feeds - array: An array of RSS feeds. + + Example context (json): + { + "feeds": [ + { + "title": "News from around my living room", + "image": { + "url": "https://www.example.com/feeds/news/poster.jpg", + "title": "Example News Logo", + "link": "https://www.example.com/feeds/news/" + }, + "items": [ + { + "id": "https://www.example.com/node/12", + "link": "https://www.example.com/my-turtle-story.html", + "title": "My Turtle Story", + "description": "This is a story about my turtle.", + "permalink": "https://www.example.com/my-turtle-story.html", + "datepublished": "11 January 2016, 7:11 pm" + }, + { + "id": "https://www.example.com/node/12", + "link": "https://www.example.com/my-cat-story.html", + "title": "My Story", + "description": "This is a story about my cats.", + "permalink": "https://www.example.com/my-cat-story.html", + "datepublished": "12 January 2016, 9:12 pm" + } + ] + }, + { + "title": "News from around my kitchen", + "image": { + "url": "https://www.example.com/feeds/news/kitchen.jpg", + "title": "Picture of My Kitchen", + "link": "https://www.example.com/feeds/news/kitchen/" + }, + "items": [ + { + "id": "https://www.example.com/node/10", + "link": "https://www.example.com/oven-smoke.html", + "title": "Why is the Oven Smoking?", + "description": "There is something smoking in the oven.", + "permalink": "https://www.example.com/oven-smoke.html", + "datepublished": "10 January 2016, 1:13 pm" + }, + { + "id": "https://www.example.com/node/13", + "link": "https://www.example.com/coffee-is-good.html", + "title": "Why My Coffee Machine is So Great!", + "description": "Don't be fancy; drips are best.", + "permalink": "https://www.example.com/oven-smoke.html", + "datepublished": "13 January 2016, 8:25 pm" + } + ] + } + ] + } +}} +{{#feeds}} + {{> block_rss_client/feed}} +{{/feeds}} diff --git a/rss_client/templates/channel_image.mustache b/rss_client/templates/channel_image.mustache new file mode 100644 index 0000000..f20166e --- /dev/null +++ b/rss_client/templates/channel_image.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_rss_client/channel_image + + Template which defines an item in an RSS Feed + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * url - string: The escaped URL of the image. + * title - string: The title of the image. + * link - string: Optionally, a URL to link the image to. Must be escaped. + + Example context (json): + { + "url": "http://www.example.com/images/catpic.jpg", + "title": "A picture of my cat", + "link": "http://www.example.com/cat-news/" + } +}} +<div class="image" title="{{title}}"> + {{#link}} + <a href="{{{link}}}"> + {{/link}} + + <img src="{{{url}}}" alt="{{title}}" /> + + {{#link}} + </a> + {{/link}} +</div> diff --git a/rss_client/templates/feed.mustache b/rss_client/templates/feed.mustache new file mode 100644 index 0000000..a69f3e8 --- /dev/null +++ b/rss_client/templates/feed.mustache @@ -0,0 +1,79 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_rss_client/feed + + Template which defines an item in an RSS Feed + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * channel_image - object: URL, title and link for the channel image. + * title - string: The title of the feed. + * items - array: An array of feed items. + + Example context (json): + { + "title": "News from around my living room", + "image": { + "url": "https://www.example.com/feeds/news/poster.jpg", + "title": "Example News Logo", + "link": "https://www.example.com/feeds/news/" + }, + "feeditems": [ + { + "id": "https://www.example.com/node/12", + "link": "https://www.example.com/my-turtle-story.html", + "title": "My Turtle Story", + "description": "This is a story about my turtle.", + "permalink": "https://www.example.com/my-turtle-story.html", + "datepublished": "11 January 2016, 7:11 pm" + }, + { + "id": "https://www.example.com/node/12", + "link": "https://www.example.com/my-cat-story.html", + "title": "My Story", + "description": "This is a story about my cats.", + "permalink": "https://www.example.com/my-cat-story.html", + "datepublished": "12 January 2016, 9:12 pm" + } + ] + } +}} +{{$image}} + {{#image}} + {{> block_rss_client/channel_image}} + {{/image}} +{{/image}} + +{{$title}} + {{#title}} + <div class="title">{{title}}</div> + {{/title}} +{{/title}} + +{{$items}} + <ul class="list no-overflow"> + {{#items}} + {{> block_rss_client/item}} + {{/items}} + </ul> +{{/items}} diff --git a/rss_client/templates/footer.mustache b/rss_client/templates/footer.mustache new file mode 100644 index 0000000..dd5d0fe --- /dev/null +++ b/rss_client/templates/footer.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more 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 block_rss_client/footer + + Template which defines an item in an RSS Feed + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * channellink - string: The channel URL. Must be escaped. + + Example context (json): + { + "channellink": "https://www.example.com/feeds/rss" + } +}} +{{#channellink}} + <a href="{{{channellink}}}">{{#str}} clientchannellink, block_rss_client {{/str}}</a> + {{#hasfailedfeeds}}<br>{{/hasfailedfeeds}} +{{/channellink}} +{{#hasfailedfeeds}} + <a href="{{{manageurl}}}">{{#str}} failedfeeds, block_rss_client {{/str}}</a> +{{/hasfailedfeeds}} \ No newline at end of file diff --git a/rss_client/templates/item.mustache b/rss_client/templates/item.mustache new file mode 100644 index 0000000..1d700ba --- /dev/null +++ b/rss_client/templates/item.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 block_rss_client/item + + Template which defines an item in an RSS Feed + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * id - string: A unique id for the feed item. + * link - string: The URL of the feed item. Must already be escaped. + * title - string: The title of the feed item. + * description - string: The text description of the feed item. + * permalink - string: The permalink of the feed item. Must already be escaped. + * datepublished - string: The date the feed item was published. + + Example context (json): + { + "id": "https://www.example.com/node", + "link": "https://www.example.com/my-cat-story.html", + "title": "My Story", + "description": "This is a story about my cats.", + "permalink": "https://www.example.com/my-cat-story.html", + "datepublished": "12 January 2016, 9:12 pm" + } +}} +<li class="p-y-1"> + {{$title}} + <div class="link"> + <a href="{{{link}}}" onclick='this.target="_blank"'>{{title}}</a> + </div> + {{/title}} + + {{$content}} + {{#description}} + <div class="date text-muted muted m-b-1"> + <small>{{{datepublished}}}</small> + </div> + <div class="description"> + {{{description}}} + </div> + {{/description}} + {{/content}} +</li> diff --git a/rss_client/tests/cron_test.php b/rss_client/tests/cron_test.php new file mode 100644 index 0000000..7f99275 --- /dev/null +++ b/rss_client/tests/cron_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/>. + +/** + * PHPunit tests for rss client cron. + * + * @package block_rss_client + * @copyright 2015 University of Nottingham + * @author Neill Magill <neill.magill@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/../../moodleblock.class.php'); +require_once(__DIR__ . '/../block_rss_client.php'); + +/** + * Class for the PHPunit tests for rss client cron. + * + * @package block_rss_client + * @copyright 2015 Universit of Nottingham + * @author Neill Magill <neill.magill@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_rss_client_cron_testcase extends advanced_testcase { + /** + * Test that when a record has a skipuntil time that is greater + * than the current time the attempt is skipped. + */ + public function test_skip() { + global $DB, $CFG; + $this->resetAfterTest(); + // Create a RSS feed record with a skip until time set to the future. + $record = (object) array( + 'userid' => 1, + 'title' => 'Skip test feed', + 'preferredtitle' => '', + 'description' => 'A feed to test the skip time.', + 'shared' => 0, + 'url' => 'http://example.com/rss', + 'skiptime' => 330, + 'skipuntil' => time() + 300, + ); + $DB->insert_record('block_rss_client', $record); + + $block = new block_rss_client(); + ob_start(); + + // Silence SimplePie php notices. + $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE); + $block->cron(); + error_reporting($errorlevel); + + $cronoutput = ob_get_clean(); + $this->assertContains('skipping until ' . userdate($record->skipuntil), $cronoutput); + $this->assertContains('0 feeds refreshed (took ', $cronoutput); + } + + /** + * Test that when a feed has an error the skip time is increaed correctly. + */ + public function test_error() { + global $DB, $CFG; + $this->resetAfterTest(); + $time = time(); + // A record that has failed before. + $record = (object) array( + 'userid' => 1, + 'title' => 'Skip test feed', + 'preferredtitle' => '', + 'description' => 'A feed to test the skip time.', + 'shared' => 0, + 'url' => 'http://example.com/rss', + 'skiptime' => 330, + 'skipuntil' => $time - 300, + ); + $record->id = $DB->insert_record('block_rss_client', $record); + + // A record that has not failed before. + $record2 = (object) array( + 'userid' => 1, + 'title' => 'Skip test feed', + 'preferredtitle' => '', + 'description' => 'A feed to test the skip time.', + 'shared' => 0, + 'url' => 'http://example.com/rss2', + 'skiptime' => 0, + 'skipuntil' => 0, + ); + $record2->id = $DB->insert_record('block_rss_client', $record2); + + // A record that is near the maximum wait time. + $record3 = (object) array( + 'userid' => 1, + 'title' => 'Skip test feed', + 'preferredtitle' => '', + 'description' => 'A feed to test the skip time.', + 'shared' => 0, + 'url' => 'http://example.com/rss3', + 'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5, + 'skipuntil' => $time - 1, + ); + $record3->id = $DB->insert_record('block_rss_client', $record3); + + // Run the cron. + $block = new block_rss_client(); + ob_start(); + + // Silence SimplePie php notices. + $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE); + $block->cron(); + error_reporting($errorlevel); + + $cronoutput = ob_get_clean(); + $skiptime1 = $record->skiptime * 2; + $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.'; + $this->assertContains($message1, $cronoutput); + $skiptime2 = 330; // Assumes that the cron time in the version file is 300. + $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.'; + $this->assertContains($message2, $cronoutput); + $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME; + $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.'; + $this->assertContains($message3, $cronoutput); + $this->assertContains('0 feeds refreshed (took ', $cronoutput); + + // Test that the records have been correctly updated. + $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id)); + $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord); + $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord); + $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id)); + $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2); + $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2); + $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id)); + $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3); + $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3); + } +} diff --git a/rss_client/tests/privacy_test.php b/rss_client/tests/privacy_test.php new file mode 100644 index 0000000..901d198 --- /dev/null +++ b/rss_client/tests/privacy_test.php @@ -0,0 +1,148 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 unit tests for block_rss_client. + * + * @package block_rss_client + * @copyright 2018 Mihail Geshoski <mihail@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\tests\provider_testcase; + +/** + * Unit tests for blocks\rss_client\classes\privacy\provider.php + * + * @copyright 2018 Mihail Geshoski <mihail@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_rss_client_testcase extends provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test getting the context for the user ID related to this plugin. + */ + public function test_get_contexts_for_userid() { + + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + + $this->add_rss_feed($user); + + $contextlist = \block_rss_client\privacy\provider::get_contexts_for_userid($user->id); + + $this->assertEquals($context, $contextlist->current()); + } + + /** + * Test that data is exported correctly for this plugin. + */ + public function test_export_user_data() { + + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + + $this->add_rss_feed($user); + $this->add_rss_feed($user); + + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + $this->export_context_data_for_user($user->id, $context, 'block_rss_client'); + + $data = $writer->get_data([get_string('pluginname', 'block_rss_client')]); + $this->assertCount(2, $data->feeds); + $feed1 = reset($data->feeds); + $this->assertEquals('BBC News - World', $feed1->title); + $this->assertEquals('World News', $feed1->preferredtitle); + $this->assertEquals('Description: BBC News - World', $feed1->description); + $this->assertEquals(get_string('no'), $feed1->shared); + $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $feed1->url); + } + + /** + * Test that user data is deleted using the context. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + + $this->add_rss_feed($user); + + // Check that we have an entry. + $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]); + $this->assertCount(1, $rssfeeds); + + \block_rss_client\privacy\provider::delete_data_for_all_users_in_context($context); + + // Check that it has now been deleted. + $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]); + $this->assertCount(0, $rssfeeds); + } + + /** + * Test that user data is deleted for this user. + */ + public function test_delete_data_for_user() { + global $DB; + + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + + $this->add_rss_feed($user); + + // Check that we have an entry. + $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]); + $this->assertCount(1, $rssfeeds); + + $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'block_rss_feed', + [$context->id]); + \block_rss_client\privacy\provider::delete_data_for_user($approvedlist); + + // Check that it has now been deleted. + $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]); + $this->assertCount(0, $rssfeeds); + } + + /** + * Add dummy rss feed. + * + * @param object $user User object + */ + private function add_rss_feed($user) { + global $DB; + + $rssfeeddata = array( + 'userid' => $user->id, + 'title' => 'BBC News - World', + 'preferredtitle' => 'World News', + 'description' => 'Description: BBC News - World', + 'shared' => 0, + 'url' => 'http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', + ); + + $DB->insert_record('block_rss_client', $rssfeeddata); + } +} diff --git a/rss_client/version.php b/rss_client/version.php new file mode 100644 index 0000000..c0e9e59 --- /dev/null +++ b/rss_client/version.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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_rss_client + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_rss_client'; // Full name of the plugin (used for diagnostics) +$plugin->cron = 300; // Set min time between cron executions to 300 secs (5 mins) diff --git a/rss_client/viewfeed.php b/rss_client/viewfeed.php new file mode 100644 index 0000000..c56eb30 --- /dev/null +++ b/rss_client/viewfeed.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/>. + +/** + * Script to let a user edit the properties of a particular RSS feed. + * + * @package block_rss_client + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir .'/simplepie/moodle_simplepie.php'); + +require_login(); +if (isguestuser()) { + print_error('guestsarenotallowed'); +} + +$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); +$courseid = optional_param('courseid', 0, PARAM_INT); +$rssid = required_param('rssid', PARAM_INT); + +if ($courseid = SITEID) { + $courseid = 0; +} +if ($courseid) { + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $PAGE->set_course($course); + $context = $PAGE->context; +} else { + $context = context_system::instance(); + $PAGE->set_context($context); +} + +$urlparams = array('rssid' => $rssid); +if ($courseid) { + $urlparams['courseid'] = $courseid; +} +if ($returnurl) { + $urlparams['returnurl'] = $returnurl; +} +$PAGE->set_url('/blocks/rss_client/viewfeed.php', $urlparams); +$PAGE->set_pagelayout('popup'); + +$rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST); + +$rss = new moodle_simplepie($rssrecord->url); + +if ($rss->error()) { + debugging($rss->error()); + print_error('errorfetchingrssfeed'); +} + +$strviewfeed = get_string('viewfeed', 'block_rss_client'); + +$PAGE->set_title($strviewfeed); +$PAGE->set_heading($strviewfeed); + +$managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams); +$PAGE->navbar->add(get_string('blocks')); +$PAGE->navbar->add(get_string('pluginname', 'block_rss_client')); +$PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds); +$PAGE->navbar->add($strviewfeed); +echo $OUTPUT->header(); + +if (!empty($rssrecord->preferredtitle)) { + $feedtitle = $rssrecord->preferredtitle; +} else { + $feedtitle = $rss->get_title(); +} +echo '<table align="center" width="50%" cellspacing="1">'."\n"; +echo '<tr><td colspan="2"><strong>'. s($feedtitle) .'</strong></td></tr>'."\n"; +foreach ($rss->get_items() as $item) { + echo '<tr><td valign="middle">'."\n"; + echo '<a href="'.$item->get_link().'" target="_blank"><strong>'; + echo s($item->get_title()); + echo '</strong></a>'."\n"; + echo '</td>'."\n"; + echo '</tr>'."\n"; + echo '<tr><td colspan="2"><small>'; + echo format_text($item->get_description(), FORMAT_HTML) .'</small></td></tr>'."\n"; +} +echo '</table>'."\n"; + +echo $OUTPUT->footer(); diff --git a/search_forums/block_search_forums.php b/search_forums/block_search_forums.php new file mode 100644 index 0000000..669d5e8 --- /dev/null +++ b/search_forums/block_search_forums.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/>. + +/** + * Block to search forum posts. + * + * @package block_search_forums + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_search_forums extends block_base { + function init() { + $this->title = get_string('pluginname', 'block_search_forums'); + } + + function get_content() { + global $CFG, $OUTPUT; + + if($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->footer = ''; + + if (empty($this->instance)) { + $this->content->text = ''; + return $this->content; + } + + $output = $this->page->get_renderer('block_search_forums'); + $searchform = new \block_search_forums\output\search_form($this->page->course->id); + $this->content->text = $output->render($searchform); + + return $this->content; + } + + function applicable_formats() { + return array('site' => true, 'course' => true); + } + + /** + * Returns the role that best describes the forum search block. + * + * @return string + */ + public function get_aria_role() { + return 'search'; + } +} + + diff --git a/search_forums/classes/output/renderer.php b/search_forums/classes/output/renderer.php new file mode 100644 index 0000000..292ce34 --- /dev/null +++ b/search_forums/classes/output/renderer.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/>. + +/** + * Block search forums renderer. + * + * @package block_search_forums + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_search_forums\output; +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use renderable; + +/** + * Block search forums renderer. + * + * @package block_search_forums + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Render search form. + * + * @param renderable $searchform The search form. + * @return string + */ + public function render_search_form(renderable $searchform) { + return $this->render_from_template('block_search_forums/search_form', $searchform->export_for_template($this)); + } + +} diff --git a/search_forums/classes/output/search_form.php b/search_forums/classes/output/search_form.php new file mode 100644 index 0000000..8213abe --- /dev/null +++ b/search_forums/classes/output/search_form.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/>. + +/** + * Search form renderable. + * + * @package block_search_forums + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_search_forums\output; +defined('MOODLE_INTERNAL') || die(); + +use help_icon; +use moodle_url; +use renderable; +use renderer_base; +use templatable; + +/** + * Search form renderable class. + * + * @package block_search_forums + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class search_form implements renderable, templatable { + + /** @var int The course ID. */ + protected $courseid; + /** @var moodle_url The form action URL. */ + protected $actionurl; + /** @var moodle_url The advanced search URL. */ + protected $advancedsearchurl; + /** @var help_icon The help icon. */ + protected $helpicon; + + /** + * Constructor. + * + * @param int $courseid The course ID. + */ + public function __construct($courseid) { + $this->courseid = $courseid; + $this->actionurl = new moodle_url('/mod/forum/search.php'); + $this->advancedsearchurl = new moodle_url('/mod/forum/search.php', ['id' => $this->courseid]); + $this->helpicon = new help_icon('search', 'core'); + } + + public function export_for_template(renderer_base $output) { + $data = [ + 'actionurl' => $this->actionurl->out(false), + 'courseid' => $this->courseid, + 'advancedsearchurl' => $this->advancedsearchurl->out(false), + 'helpicon' => $this->helpicon->export_for_template($output), + ]; + return $data; + } + +} diff --git a/search_forums/classes/privacy/provider.php b/search_forums/classes/privacy/provider.php new file mode 100644 index 0000000..090f960 --- /dev/null +++ b/search_forums/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_search_forums. + * + * @package block_search_forums + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_search_forums\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_search_forums implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/search_forums/db/access.php b/search_forums/db/access.php new file mode 100644 index 0000000..04d96db --- /dev/null +++ b/search_forums/db/access.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/>. + +/** + * Search forums block caps. + * + * @package block_search_forums + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/search_forums: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/search_forums/lang/en/block_search_forums.php b/search_forums/lang/en/block_search_forums.php new file mode 100644 index 0000000..c87b740 --- /dev/null +++ b/search_forums/lang/en/block_search_forums.php @@ -0,0 +1,28 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_search_forums', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_search_forums + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['advancedsearch'] = 'Advanced search'; +$string['pluginname'] = 'Search forums'; +$string['search_forums:addinstance'] = 'Add a new search forums block'; +$string['privacy:metadata'] = 'The Search forums block only shows data stored in other locations.'; diff --git a/search_forums/styles.css b/search_forums/styles.css new file mode 100644 index 0000000..09217a5 --- /dev/null +++ b/search_forums/styles.css @@ -0,0 +1,16 @@ +.block_search_forums .searchform { + text-align: center; +} + +.block_search_forums .searchform img { + vertical-align: middle; +} + +.block_search_forums .searchform img.resize { + width: 1em; + height: 1.1em; +} + +.block_search_forums .invisiblefieldset { + display: block; +} \ No newline at end of file diff --git a/search_forums/templates/search_form.mustache b/search_forums/templates/search_form.mustache new file mode 100644 index 0000000..9f0411f --- /dev/null +++ b/search_forums/templates/search_form.mustache @@ -0,0 +1,15 @@ +<div class="searchform"> + <form action="{{actionurl}}" style="display: inline;"> + <fieldset class="invisiblefieldset"> + <legend class="accesshide">{{#str}}search{{/str}}</legend> + <input type="hidden" name="id" value="{{courseid}}"> + <label class="accesshide" for="searchform_search">{{#str}}search{{/str}}</label> + <input id="searchform_search" name="search" type="text" size="16"> + <button id="searchform_button" type="submit" title={{#quote}}{{#str}}search{{/str}}{{/quote}}>{{#str}}go{{/str}}</button><br> + <a href="{{advancedsearchurl}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a> + {{#helpicon}} + {{>core/help_icon}} + {{/helpicon}} + </fieldset> + </form> +</div> diff --git a/search_forums/tests/behat/block_search_forums_course.feature b/search_forums/tests/behat/block_search_forums_course.feature new file mode 100644 index 0000000..e2e37d2 --- /dev/null +++ b/search_forums/tests/behat/block_search_forums_course.feature @@ -0,0 +1,71 @@ +@block @block_search_forums @mod_forum +Feature: The search forums block allows users to search for forum posts on course page + In order to search for a forum post + As a user + I can use the search forums block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + 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 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" node in "Course administration" + And I set the field "id_newsitems" to "1" + And I press "Save and display" + And I turn editing mode on + And I add the "Latest announcements" block + And I add the "Search forums" block + And I log out + + Scenario: Use the search forum block in a course without any forum posts + Given I log in as "student1" + And I am on "Course 1" course homepage + When I set the following fields to these values: + | searchform_search | Moodle | + And I press "Go" + Then I should see "No posts" + + Scenario: Use the search forum block in a course with a hidden forum and search for posts + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I add a new topic to "Announcements" forum with: + | Subject | My subject | + | Message | My message | + And I am on "Course 1" course homepage with editing mode on + And I follow "Announcements" + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I set the field "id_visible" to "0" + And I press "Save and return to course" + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And "Search forums" "block" should exist + And I set the following fields to these values: + | searchform_search | message | + And I press "Go" + Then I should see "No posts" + + Scenario: Use the search forum block in a course and search for posts + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I add a new topic to "Announcements" forum with: + | Subject | My subject | + | Message | My message | + And I log out + When I log in as "student1" + And I am on "Course 1" course homepage + And "Search forums" "block" should exist + And I set the following fields to these values: + | searchform_search | message | + And I press "Go" + Then I should see "My subject" diff --git a/search_forums/tests/behat/block_search_forums_frontpage.feature b/search_forums/tests/behat/block_search_forums_frontpage.feature new file mode 100644 index 0000000..1f76d15 --- /dev/null +++ b/search_forums/tests/behat/block_search_forums_frontpage.feature @@ -0,0 +1,31 @@ +@block @block_search_forums @mod_forum +Feature: The search forums block allows users to search for forum posts on frontpage + In order to search for a forum post + As an administrator + I can add the search forums block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Search forums" block + And I log out + + Scenario: Use the search forum block on the frontpage and search for posts as a user + Given I log in as "student1" + And I am on site homepage + When I set the following fields to these values: + | searchform_search | Moodle | + And I press "Go" + Then I should see "No posts" + + Scenario: Use the search forum block on the frontpage and search for posts as a guest + Given I log in as "guest" + And I am on site homepage + When I set the following fields to these values: + | searchform_search | Moodle | + And I press "Go" + Then I should see "No posts" diff --git a/search_forums/version.php b/search_forums/version.php new file mode 100644 index 0000000..e62c1d8 --- /dev/null +++ b/search_forums/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 details + * + * @package block_search_forums + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_search_forums'; // Full name of the plugin (used for diagnostics) + +$plugin->dependencies = array('mod_forum' => 2018050800); diff --git a/section_links/block_section_links.php b/section_links/block_section_links.php new file mode 100644 index 0000000..7f12683 --- /dev/null +++ b/section_links/block_section_links.php @@ -0,0 +1,159 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 main class for the section links block. + * + * @package block_section_links + * @copyright Jason Hardin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Section links block class. + * + * @package block_section_links + * @copyright Jason Hardin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_section_links extends block_base { + + /** + * Initialises the block instance. + */ + public function init() { + $this->title = get_string('pluginname', 'block_section_links'); + } + + /** + * Returns an array of formats for which this block can be used. + * + * @return array + */ + public function applicable_formats() { + return array( + 'course-view-weeks' => true, + 'course-view-topics' => true + ); + } + + /** + * Generates the content of the block and returns it. + * + * If the content has already been generated then the previously generated content is returned. + * + * @return stdClass + */ + public function get_content() { + + // The config should be loaded by now. + // If its empty then we will use the global config for the section links block. + if (isset($this->config)){ + $config = $this->config; + } else{ + $config = get_config('block_section_links'); + } + + if ($this->content !== null) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->footer = ''; + $this->content->text = ''; + + if (empty($this->instance)) { + return $this->content; + } + + $course = $this->page->course; + $courseformat = course_get_format($course); + $numsections = $courseformat->get_last_section_number(); + $context = context_course::instance($course->id); + + // Course format options 'numsections' is required to display the block. + if (empty($numsections)) { + return $this->content; + } + + // Prepare the increment value. + if (!empty($config->numsections1) and ($numsections > $config->numsections1)) { + $inc = $config->incby1; + } else if ($numsections > 22) { + $inc = 2; + } else { + $inc = 1; + } + if (!empty($config->numsections2) and ($numsections > $config->numsections2)) { + $inc = $config->incby2; + } else { + if ($numsections > 40) { + $inc = 5; + } + } + + // Prepare an array of sections to create links for. + $sections = array(); + $canviewhidden = has_capability('moodle/course:update', $context); + $coursesections = $courseformat->get_sections(); + $coursesectionscount = count($coursesections); + $sectiontojumpto = false; + for ($i = $inc; $i <= $coursesectionscount; $i += $inc) { + if ($i > $numsections || !isset($coursesections[$i])) { + continue; + } + $section = $coursesections[$i]; + if ($section->section && ($section->visible || $canviewhidden)) { + $sections[$i] = (object)array( + 'section' => $section->section, + 'visible' => $section->visible, + 'highlight' => false + ); + if ($courseformat->is_section_current($section)) { + $sections[$i]->highlight = true; + $sectiontojumpto = $section->section; + } + } + } + + if (!empty($sections)) { + // Render the sections. + $renderer = $this->page->get_renderer('block_section_links'); + $this->content->text = $renderer->render_section_links($this->page->course, $sections, $sectiontojumpto); + } + + return $this->content; + } + /** + * Returns true if this block has instance config. + * + * @return bool + **/ + public function instance_allow_config() { + return true; + } + + /** + * Returns true if this block has global config. + * + * @return bool + */ + public function has_config() { + return true; + } +} + + diff --git a/section_links/classes/privacy/provider.php b/section_links/classes/privacy/provider.php new file mode 100644 index 0000000..83bb444 --- /dev/null +++ b/section_links/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_section_links. + * + * @package block_section_links + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_section_links\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_section_links implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/section_links/db/access.php b/section_links/db/access.php new file mode 100644 index 0000000..7a96c63 --- /dev/null +++ b/section_links/db/access.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/>. + +/** + * Section links block caps. + * + * @package block_section_links + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/section_links: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/section_links/db/upgrade.php b/section_links/db/upgrade.php new file mode 100644 index 0000000..0200032 --- /dev/null +++ b/section_links/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the section links block + * + * 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. + * + * @since Moodle 2.5 + * @package block_section_links + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade code for the section links block. + * + * @global moodle_database $DB + * @param int $oldversion + * @param object $block + */ +function xmldb_block_section_links_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/section_links/edit_form.php b/section_links/edit_form.php new file mode 100644 index 0000000..63a3593 --- /dev/null +++ b/section_links/edit_form.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/>. + +/** + * Instance configuration for the section links block. + * + * @package block_section_links + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Instance configuration form. + * + * @package block_section_links + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_section_links_edit_form extends block_edit_form { + + /** + * The definition of the fields to use. + * + * @param MoodleQuickForm $mform + */ + protected function specific_definition($mform) { + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $numberofsections = array(); + for ($i = 1; $i < 53; $i++){ + $numberofsections[$i] = $i; + } + + $increments = array(); + + for ($i = 1; $i < 11; $i++){ + $increments[$i] = $i; + } + + $config = get_config('block_section_links'); + + $selected = array( + 1 => array(22, 2), + 2 => array(40, 5), + ); + if (!empty($config->numsections1)) { + if (empty($config->incby1)) { + $config->incby1 = $selected[1][1]; + } + $selected[1] = array($config->numsections1, $config->incby1); + } + + if (!empty($config->numsections2)) { + if (empty($config->incby1)) { + $config->incby1 = $selected[2][1]; + } + $selected[2] = array($config->numsections2, $config->incby2); + } + + for ($i = 1; $i < 3; $i++) { + $mform->addElement('select', 'config_numsections'.$i, get_string('numsections'.$i, 'block_section_links'), $numberofsections); + $mform->setDefault('config_numsections'.$i, $selected[$i][0]); + $mform->setType('config_numsections'.$i, PARAM_INT); + $mform->addHelpButton('config_numsections'.$i, 'numsections'.$i, 'block_section_links'); + + $mform->addElement('select', 'config_incby'.$i, get_string('incby'.$i, 'block_section_links'), $increments); + $mform->setDefault('config_incby'.$i, $selected[$i][1]); + $mform->setType('config_incby'.$i, PARAM_INT); + $mform->addHelpButton('config_incby'.$i, 'incby'.$i, 'block_section_links'); + } + + } +} \ No newline at end of file diff --git a/section_links/lang/en/block_section_links.php b/section_links/lang/en/block_section_links.php new file mode 100644 index 0000000..b267898 --- /dev/null +++ b/section_links/lang/en/block_section_links.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/>. + +/** + * Strings for component 'block_section_links', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_section_links + * @copyright Jason Hardin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['incby1'] = 'Increase by'; +$string['incby1_help'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.'; +$string['incby2'] = 'Alternative increase by'; +$string['incby2_help'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.'; +$string['jumptocurrenttopic'] = 'Jump to current topic'; +$string['jumptocurrentweek'] = 'Jump to current week'; +$string['numsections1'] = 'Number of sections'; +$string['numsections1_help'] = 'Once the number of sections in the course reaches this number then the increment by value is used.'; +$string['numsections2'] = 'Alternative number of sections'; +$string['numsections2_help'] = 'Once the number of sections in the course reaches this number then the Alternative increment by value is used.'; +$string['pluginname'] = 'Section links'; +$string['section_links:addinstance'] = 'Add a new section links block'; +$string['topics'] = 'Topics'; +$string['weeks'] = 'Weeks'; +$string['privacy:metadata'] = 'The Section links block only shows data stored in other locations.'; diff --git a/section_links/renderer.php b/section_links/renderer.php new file mode 100644 index 0000000..338855b --- /dev/null +++ b/section_links/renderer.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/>. + +/** + * Renderer for the section links block. + * + * @since Moodle 2.5 + * @package block_section_links + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Renderer for the section links block. + * + * @package block_section_links + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_section_links_renderer extends plugin_renderer_base { + + /** + * Render a series of section links. + * + * @param stdClass $course The course we are rendering for. + * @param array $sections An array of section objects to render. + * @param bool|int The section to provide a jump to link for. + * @return string The HTML to display. + */ + public function render_section_links(stdClass $course, array $sections, $jumptosection = false) { + $html = html_writer::start_tag('ol', array('class' => 'inline-list')); + foreach ($sections as $section) { + $attributes = array(); + if (!$section->visible) { + $attributes['class'] = 'dimmed'; + } + $html .= html_writer::start_tag('li'); + $sectiontext = $section->section; + if ($section->highlight) { + $sectiontext = html_writer::tag('strong', $sectiontext); + } + $html .= html_writer::link(course_get_url($course, $section->section), $sectiontext, $attributes); + $html .= html_writer::end_tag('li').' '; + } + $html .= html_writer::end_tag('ol'); + if ($jumptosection && isset($sections[$jumptosection])) { + + if ($course->format == 'weeks') { + $linktext = new lang_string('jumptocurrentweek', 'block_section_links'); + } else if ($course->format == 'topics') { + $linktext = new lang_string('jumptocurrenttopic', 'block_section_links'); + } + + $attributes = array(); + if (!$sections[$jumptosection]->visible) { + $attributes['class'] = 'dimmed'; + } + $html .= html_writer::link(course_get_url($course, $jumptosection), $linktext, $attributes); + } + + return $html; + } +} \ No newline at end of file diff --git a/section_links/settings.php b/section_links/settings.php new file mode 100644 index 0000000..2fcb4a4 --- /dev/null +++ b/section_links/settings.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/>. + +/** + * Section links block + * + * @package block_section_links + * @copyright Jason Hardin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $numberofsections = array(); + + for ($i = 1; $i < 53; $i++){ + $numberofsections[$i] = $i; + } + $increments = array(); + + for ($i = 1; $i < 11; $i++){ + $increments[$i] = $i; + } + + $selected = array(1 => array(22,2), + 2 => array(40,5)); + + for($i = 1; $i < 3; $i++){ + $settings->add(new admin_setting_configselect('block_section_links/numsections'.$i, get_string('numsections'.$i, 'block_section_links'), + get_string('numsections'.$i.'_help', 'block_section_links'), + $selected[$i][0], $numberofsections)); + + $settings->add(new admin_setting_configselect('block_section_links/incby'.$i, get_string('incby'.$i, 'block_section_links'), + get_string('incby'.$i.'_help', 'block_section_links'), + $selected[$i][1], $increments)); + } +} \ No newline at end of file diff --git a/section_links/tests/behat/block_section_links_course.feature b/section_links/tests/behat/block_section_links_course.feature new file mode 100644 index 0000000..5ef506d --- /dev/null +++ b/section_links/tests/behat/block_section_links_course.feature @@ -0,0 +1,57 @@ +@block @block_section_links +Feature: The section links block allows users to quickly navigate around a moodle course + In order to navigate a moodle course + As a teacher + I can use the section links block + + Background: + Given the following "courses" exist: + | fullname | shortname | category | numsections | coursedisplay | + | Course 1 | C1 | 0 | 20 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Assignment" to section "5" and I fill the form with: + | Assignment name | Test assignment 1 | + | Description | Offline text | + | assignsubmission_file_enabled | 0 | + + Scenario: Add the section links block to a course. + Given I add the "Section links" block + And I turn editing mode off + And I should see "5" in the "Section links" "block" + When I follow "5" + Then I should see "Test assignment 1" + + Scenario: Add the section links block to a course and limit the sections displayed. + Given I add the "Section links" block + And I configure the "Section links" block + And I set the following fields to these values: + | id_config_numsections1 | 5 | + | id_config_incby1 | 5 | + | id_config_numsections2 | 40 | + | id_config_incby2 | 10 | + And I press "Save changes" + And I turn editing mode off + And I should see "5" in the "Section links" "block" + When I follow "5" + Then I should see "Test assignment 1" + + Scenario: Add the section links block to a course and limit the sections displayed using the alternative number of sections. + Given I add the "Section links" block + And I configure the "Section links" block + And I set the following fields to these values: + | id_config_numsections1 | 5 | + | id_config_incby1 | 1 | + | id_config_numsections2 | 10 | + | id_config_incby2 | 5 | + And I press "Save changes" + And I turn editing mode off + And I should see "5" in the "Section links" "block" + When I follow "5" + Then I should see "Test assignment 1" diff --git a/section_links/version.php b/section_links/version.php new file mode 100644 index 0000000..8f9aa4f --- /dev/null +++ b/section_links/version.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/>. + +/** + * Version details + * + * @package block_section_links + * @copyright Jason Hardin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_section_links'; // Full name of the plugin (used for diagnostics) diff --git a/selfcompletion/block_selfcompletion.php b/selfcompletion/block_selfcompletion.php new file mode 100644 index 0000000..4e4d2d7 --- /dev/null +++ b/selfcompletion/block_selfcompletion.php @@ -0,0 +1,112 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Self completion block. + * + * @package block_selfcompletion + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->libdir.'/completionlib.php'); + +/** + * Self course completion marking + * Let's a user manually complete a course + * + * Will only display if the course has completion enabled, + * there is a self completion criteria, and the logged in user is yet + * to complete the course. + */ +class block_selfcompletion extends block_base { + + public function init() { + $this->title = get_string('pluginname', 'block_selfcompletion'); + } + + function applicable_formats() { + return array('course' => true); + } + + public function get_content() { + global $CFG, $USER; + + // If content is cached + if ($this->content !== NULL) { + return $this->content; + } + + // Create empty content + $this->content = new stdClass; + + // Can edit settings? + $can_edit = has_capability('moodle/course:update', context_course::instance($this->page->course->id)); + + // Get course completion data + $info = new completion_info($this->page->course); + + // Don't display if completion isn't enabled! + if (!completion_info::is_enabled_for_site()) { + if ($can_edit) { + $this->content->text = get_string('completionnotenabledforsite', 'completion'); + } + return $this->content; + + } else if (!$info->is_enabled()) { + if ($can_edit) { + $this->content->text = get_string('completionnotenabledforcourse', 'completion'); + } + return $this->content; + } + + // Get this user's data + $completion = $info->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF); + + // Check if self completion is one of this course's criteria + if (empty($completion)) { + if ($can_edit) { + $this->content->text = get_string('selfcompletionnotenabled', 'block_selfcompletion'); + } + return $this->content; + } + + // Check this user is enroled + if (!$info->is_tracked_user($USER->id)) { + $this->content->text = get_string('nottracked', 'completion'); + return $this->content; + } + + // Is course complete? + if ($info->is_course_complete($USER->id)) { + $this->content->text = get_string('coursealreadycompleted', 'completion'); + return $this->content; + + // Check if the user has already marked themselves as complete + } else if ($completion->is_complete()) { + $this->content->text = get_string('alreadyselfcompleted', 'block_selfcompletion'); + return $this->content; + + // If user is not complete, or has not yet self completed + } else { + $this->content->text = ''; + $this->content->footer = '<br /><a href="'.$CFG->wwwroot.'/course/togglecompletion.php?course='.$this->page->course->id.'">'; + $this->content->footer .= get_string('completecourse', 'block_selfcompletion').'</a>...'; + } + + return $this->content; + } +} diff --git a/selfcompletion/classes/privacy/provider.php b/selfcompletion/classes/privacy/provider.php new file mode 100644 index 0000000..c96a187 --- /dev/null +++ b/selfcompletion/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_selfcompletion. + * + * @package block_selfcompletion + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_selfcompletion\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_selfcompletion implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/selfcompletion/db/access.php b/selfcompletion/db/access.php new file mode 100644 index 0000000..d91d4d4 --- /dev/null +++ b/selfcompletion/db/access.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/>. + +/** + * Self completion block caps. + * + * @package block_selfcompletion + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/selfcompletion: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/selfcompletion/db/upgrade.php b/selfcompletion/db/upgrade.php new file mode 100644 index 0000000..7e5e56a --- /dev/null +++ b/selfcompletion/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the self completion block + * + * 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. + * + * @since Moodle 2.0 + * @package block_selfcompletion + * @copyright 2012 Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Handles upgrading instances of this block. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_selfcompletion_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/selfcompletion/lang/en/block_selfcompletion.php b/selfcompletion/lang/en/block_selfcompletion.php new file mode 100644 index 0000000..2561b6a --- /dev/null +++ b/selfcompletion/lang/en/block_selfcompletion.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_selfcompletion', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_selfcompletion + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['alreadyselfcompleted'] = 'You have already marked yourself as complete in this course'; +$string['completecourse'] = 'Complete course'; +$string['pluginname'] = 'Self completion'; +$string['selfcompletionnotenabled'] = 'The self completion criteria has not been enabled for this course'; +$string['selfcompletion:addinstance'] = 'Add a new self completion block'; +$string['privacy:metadata'] = 'The Self completion block only shows data stored in other locations.'; diff --git a/selfcompletion/version.php b/selfcompletion/version.php new file mode 100644 index 0000000..07db48d --- /dev/null +++ b/selfcompletion/version.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/>. + +/** + * Version details + * + * @package block_selfcompletion + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_selfcompletion'; // Full name of the plugin (used for diagnostics) diff --git a/settings/amd/build/settingsblock.min.js b/settings/amd/build/settingsblock.min.js new file mode 100644 index 0000000..1cebc85 --- /dev/null +++ b/settings/amd/build/settingsblock.min.js @@ -0,0 +1 @@ +define(["jquery","core/tree"],function(a,b){return{init:function(a,c){var d=new b(".block_settings .block_tree");if(c){var e=d.treeRoot.find("#"+c),f=e.children("a").first();f.replaceWith('<span tabindex="0">'+f.html()+"</span>")}d.finishExpandingGroup=function(c){b.prototype.finishExpandingGroup.call(this,c),Y.use("moodle-core-event",function(){Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED,{instanceid:a})})},d.collapseGroup=function(c){b.prototype.collapseGroup.call(this,c),Y.use("moodle-core-event",function(){Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED,{instanceid:a})})}}}}); \ No newline at end of file diff --git a/settings/amd/src/settingsblock.js b/settings/amd/src/settingsblock.js new file mode 100644 index 0000000..f17553f --- /dev/null +++ b/settings/amd/src/settingsblock.js @@ -0,0 +1,51 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Load the settings block tree javscript + * + * @module block_settings/settingsblock + * @package core + * @copyright 2015 John Okely <john@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/tree'], function($, Tree) { + return { + init: function(instanceid, siteAdminNodeId) { + var adminTree = new Tree(".block_settings .block_tree"); + if (siteAdminNodeId) { + var siteAdminNode = adminTree.treeRoot.find('#' + siteAdminNodeId); + var siteAdminLink = siteAdminNode.children('a').first(); + siteAdminLink.replaceWith('<span tabindex="0">' + siteAdminLink.html() + '</span>'); + } + adminTree.finishExpandingGroup = function(item) { + Tree.prototype.finishExpandingGroup.call(this, item); + Y.use('moodle-core-event', function() { + Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, { + instanceid: instanceid + }); + }); + }; + adminTree.collapseGroup = function(item) { + Tree.prototype.collapseGroup.call(this, item); + Y.use('moodle-core-event', function() { + Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, { + instanceid: instanceid + }); + }); + }; + } + }; +}); diff --git a/settings/block_settings.php b/settings/block_settings.php new file mode 100644 index 0000000..1ed9409 --- /dev/null +++ b/settings/block_settings.php @@ -0,0 +1,163 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 classes used to manage the navigation structures in Moodle + * and was introduced as part of the changes occuring in Moodle 2.0 + * + * @since Moodle 2.0 + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The settings navigation tree block class + * + * Used to produce the settings navigation block new to Moodle 2.0 + * + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_settings extends block_base { + + /** @var string */ + public static $navcount; + public $blockname = null; + /** @var bool */ + protected $contentgenerated = false; + /** @var bool|null */ + protected $docked = null; + + /** + * Set the initial properties for the block + */ + function init() { + $this->blockname = get_class($this); + $this->title = get_string('pluginname', $this->blockname); + } + + /** + * All multiple instances of this block + * @return bool Returns true + */ + function instance_allow_multiple() { + return false; + } + + /** + * The settings block cannot be hidden by default as it is integral to + * the navigation of Moodle. + * + * @return false + */ + function instance_can_be_hidden() { + return false; + } + + /** + * Set the applicable formats for this block to all + * @return array + */ + function applicable_formats() { + return array('all' => true); + } + + /** + * Allow the user to configure a block instance + * @return bool Returns true + */ + function instance_allow_config() { + return true; + } + + function instance_can_be_docked() { + return (parent::instance_can_be_docked() && (empty($this->config->enabledock) || $this->config->enabledock=='yes')); + } + + function get_required_javascript() { + global $PAGE; + $adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN); + parent::get_required_javascript(); + $arguments = array( + 'instanceid' => $this->instance->id, + 'adminnodeid' => $adminnode ? $adminnode->id : null + ); + $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', $arguments); + } + + /** + * Gets the content for this block by grabbing it from $this->page + */ + function get_content() { + global $CFG, $OUTPUT; + // First check if we have already generated, don't waste cycles + if ($this->contentgenerated === true) { + return true; + } + // JS for navigation moved to the standard theme, the code will probably have to depend on the actual page structure + // $this->page->requires->js('/lib/javascript-navigation.js'); + block_settings::$navcount++; + + // Check if this block has been docked + if ($this->docked === null) { + $this->docked = get_user_preferences('nav_in_tab_panel_settingsnav'.block_settings::$navcount, 0); + } + + // Check if there is a param to change the docked state + if ($this->docked && optional_param('undock', null, PARAM_INT)==$this->instance->id) { + unset_user_preference('nav_in_tab_panel_settingsnav'.block_settings::$navcount, 0); + $url = $this->page->url; + $url->remove_params(array('undock')); + redirect($url); + } else if (!$this->docked && optional_param('dock', null, PARAM_INT)==$this->instance->id) { + set_user_preferences(array('nav_in_tab_panel_settingsnav'.block_settings::$navcount=>1)); + $url = $this->page->url; + $url->remove_params(array('dock')); + redirect($url); + } + + $renderer = $this->page->get_renderer('block_settings'); + $this->content = new stdClass(); + $this->content->text = $renderer->settings_tree($this->page->settingsnav); + + // only do search if you have moodle/site:config + if (!empty($this->content->text)) { + if (has_capability('moodle/site:config',context_system::instance()) ) { + $this->content->footer = $renderer->search_form(new moodle_url("$CFG->wwwroot/$CFG->admin/search.php"), optional_param('query', '', PARAM_RAW)); + } else { + $this->content->footer = ''; + } + + if (!empty($this->config->enabledock) && $this->config->enabledock == 'yes') { + user_preference_allow_ajax_update('nav_in_tab_panel_settingsnav'.block_settings::$navcount, PARAM_INT); + } + } + + $this->contentgenerated = true; + return true; + } + + /** + * Returns the role that best describes the settings block. + * + * @return string 'navigation' + */ + public function get_aria_role() { + return 'navigation'; + } +} diff --git a/settings/classes/privacy/provider.php b/settings/classes/privacy/provider.php new file mode 100644 index 0000000..808dda4 --- /dev/null +++ b/settings/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_settings. + * + * @package block_settings + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_settings\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_settings implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/settings/db/access.php b/settings/db/access.php new file mode 100644 index 0000000..7585857 --- /dev/null +++ b/settings/db/access.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/>. + +/** + * Settings block caps. + * + * @package block_settings + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/settings:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/settings: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/settings/db/upgrade.php b/settings/db/upgrade.php new file mode 100644 index 0000000..2a9dceb --- /dev/null +++ b/settings/db/upgrade.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/>. + +/** + * This file keeps track of upgrades to the settings block + * + * 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. + * + * @since Moodle 2.0 + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * As of the implementation of this block and the general navigation code + * in Moodle 2.0 the body of immediate upgrade work for this block and + * settings is done in core upgrade {@see lib/db/upgrade.php} + * + * There were several reasons that they were put there and not here, both becuase + * the process for the two blocks was very similar and because the upgrade process + * was complex due to us wanting to remvoe the outmoded blocks that this + * block was going to replace. + * + * @param int $oldversion + * @param object $block + */ +function xmldb_block_settings_upgrade($oldversion, $block) { + global $CFG; + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/settings/edit_form.php b/settings/edit_form.php new file mode 100644 index 0000000..0874ee6 --- /dev/null +++ b/settings/edit_form.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/>. + +/** + * Form for editing settings navigation instances. + * + * @since Moodle 2.0 + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for setting navigation instances. + * + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_settings_edit_form extends block_edit_form { + protected function specific_definition($mform) { + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $yesnooptions = array('yes'=>get_string('yes'), 'no'=>get_string('no')); + + $mform->addElement('select', 'config_enabledock', get_string('enabledock', $this->block->blockname), $yesnooptions); + if (empty($this->block->config->enabledock) || $this->block->config->enabledock=='yes') { + $mform->getElement('config_enabledock')->setSelected('yes'); + } else { + $mform->getElement('config_enabledock')->setSelected('no'); + } + } +} \ No newline at end of file diff --git a/settings/lang/en/block_settings.php b/settings/lang/en/block_settings.php new file mode 100644 index 0000000..80e95b2 --- /dev/null +++ b/settings/lang/en/block_settings.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/>. + +/** + * This file contains language strings used in the settings navigation block + * + * @since Moodle 2.0 + * @package block_settings + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['enabledock'] = 'Allow the user to dock this block'; +$string['pluginname'] = 'Administration'; +$string['settings:addinstance'] = 'Add a new administration block'; +$string['settings:myaddinstance'] = 'Add a new administration block to Dashboard'; +$string['privacy:metadata'] = 'The Administration block only shows data stored in other locations.'; diff --git a/settings/renderer.php b/settings/renderer.php new file mode 100644 index 0000000..34e99ef --- /dev/null +++ b/settings/renderer.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/>. + +/** + * Settings block + * + * @package block_settings + * @copyright 2010 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_settings_renderer extends plugin_renderer_base { + + public function settings_tree(settings_navigation $navigation) { + $count = 0; + foreach ($navigation->children as &$child) { + $child->preceedwithhr = ($count!==0); + if ($child->display) { + $count++; + } + } + $navigationattrs = array( + 'class' => 'block_tree list', + 'role' => 'tree', + 'data-ajax-loader' => 'block_navigation/site_admin_loader'); + $content = $this->navigation_node($navigation, $navigationattrs); + if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) { + $content = $this->output->box($content, 'block_tree_box', $navigation->id); + } + return $content; + } + + /** + * Build the navigation node. + * + * @param navigation_node $node the navigation node object. + * @param array $attrs list of attributes. + * @param int $depth the depth, default to 1. + * @return string the navigation node code. + */ + protected function navigation_node(navigation_node $node, $attrs=array(), $depth = 1) { + $items = $node->children; + + // exit if empty, we don't want an empty ul element + if ($items->count()==0) { + return ''; + } + + // array of nested li elements + $lis = array(); + $number = 0; + foreach ($items as $item) { + $number++; + if (!$item->display) { + continue; + } + + $isbranch = ($item->children->count()>0 || $item->nodetype==navigation_node::NODETYPE_BRANCH); + + if ($isbranch) { + $item->hideicon = true; + } + + $content = $this->output->render($item); + $id = $item->id ? $item->id : html_writer::random_id(); + $ulattr = ['id' => $id . '_group', 'role' => 'group']; + $liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'tabindex' => '-1']; + $pattr = ['class' => ['tree_item'], 'role' => 'treeitem']; + $pattr += !empty($item->id) ? ['id' => $item->id] : []; + $hasicon = (!$isbranch && $item->icon instanceof renderable); + + if ($isbranch) { + $liattr['class'][] = 'contains_branch'; + if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count() == 0 + && $item->nodetype == navigation_node::NODETYPE_BRANCH)) { + $pattr += ['aria-expanded' => 'false']; + } else { + $pattr += ['aria-expanded' => 'true']; + } + if ($item->requiresajaxloading) { + $pattr['data-requires-ajax'] = 'true'; + $pattr['data-loaded'] = 'false'; + } else { + $pattr += ['aria-owns' => $id . '_group']; + } + } else if ($hasicon) { + $liattr['class'][] = 'item_with_icon'; + $pattr['class'][] = 'hasicon'; + } + if ($item->isactive === true) { + $liattr['class'][] = 'current_branch'; + } + if (!empty($item->classes) && count($item->classes) > 0) { + $pattr['class'] = array_merge($pattr['class'], $item->classes); + } + $nodetextid = 'label_' . $depth . '_' . $number; + + // class attribute on the div item which only contains the item content + $pattr['class'][] = 'tree_item'; + if ($isbranch) { + $pattr['class'][] = 'branch'; + } else { + $pattr['class'][] = 'leaf'; + } + + $liattr['class'] = join(' ', $liattr['class']); + $pattr['class'] = join(' ', $pattr['class']); + + if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') { + $ulattr += ['aria-hidden' => 'true']; + } + + $content = html_writer::tag('p', $content, $pattr) . $this->navigation_node($item, $ulattr, $depth + 1); + if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) { + $content = html_writer::empty_tag('hr') . $content; + } + $liattr['aria-labelledby'] = $nodetextid; + $content = html_writer::tag('li', $content, $liattr); + $lis[] = $content; + } + + if (count($lis)) { + if (empty($attrs['role'])) { + $attrs['role'] = 'group'; + } + return html_writer::tag('ul', implode("\n", $lis), $attrs); + } else { + return ''; + } + } + + public function search_form(moodle_url $formtarget, $searchvalue) { + $content = html_writer::start_tag('form', array('class'=>'adminsearchform', 'method'=>'get', 'action'=>$formtarget, 'role' => 'search')); + $content .= html_writer::start_tag('div'); + $content .= html_writer::tag('label', s(get_string('searchinsettings', 'admin')), array('for'=>'adminsearchquery', 'class'=>'accesshide')); + $content .= html_writer::empty_tag('input', array('id'=>'adminsearchquery', 'type'=>'text', 'name'=>'query', 'value'=>s($searchvalue))); + $content .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>s(get_string('search')))); + $content .= html_writer::end_tag('div'); + $content .= html_writer::end_tag('form'); + return $content; + } + +} diff --git a/settings/styles.css b/settings/styles.css new file mode 100644 index 0000000..3d7e1d1 --- /dev/null +++ b/settings/styles.css @@ -0,0 +1,65 @@ +.block_settings .block_tree ul { + margin-left: 18px; +} + +.block_settings .block_tree p.hasicon { + text-indent: -21px; + padding-left: 21px; +} + +.block_settings .block_tree p.hasicon img { + width: 16px; + height: 16px; + margin-top: 3px; + margin-right: 5px; + vertical-align: top; +} + +.block_settings .block_tree p.hasicon.visibleifjs { + display: block; +} + +.block_settings .block_tree .tree_item.branch { + padding-left: 21px; +} + +.block_settings .block_tree .tree_item { + cursor: pointer; + margin: 3px 0; + background-position: 0 50%; + background-repeat: no-repeat; +} + +.block_settings .block_tree .active_tree_node { + font-weight: bold; +} + +.block_settings .block_tree [aria-expanded="true"] { + background-image: url('[[pix:t/expanded]]'); +} + +.block_settings .block_tree [aria-expanded="false"] { + background-image: url('[[pix:t/collapsed]]'); +} + +.block_settings .block_tree [aria-expanded="true"].emptybranch { + background-image: url('[[pix:t/collapsed_empty]]'); +} + +.block_settings .block_tree [aria-expanded="false"].loading { + background-image: url('[[pix:i/loading_small]]'); +} +/*rtl:raw: +.block_settings .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed_rtl]]');} +.block_settings .block_tree [aria-expanded="true"].emptybranch {background-image: url('[[pix:t/collapsed_empty_rtl]]');} +.block_settings .block_tree [aria-expanded="false"].loading {background-image: url('[[pix:i/loading_small]]');} +*/ +.block_settings .block_tree [aria-hidden="false"] { + display: block; +} + +.block_settings .block_tree [aria-hidden="true"]:not(.icon) { + display: none; +} + + diff --git a/settings/version.php b/settings/version.php new file mode 100644 index 0000000..9540efc --- /dev/null +++ b/settings/version.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/>. + +/** + * Version details + * + * @package block_settings + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_settings'; // Full name of the plugin (used for diagnostics) diff --git a/site_main_menu/block_site_main_menu.php b/site_main_menu/block_site_main_menu.php new file mode 100644 index 0000000..c5a692f --- /dev/null +++ b/site_main_menu/block_site_main_menu.php @@ -0,0 +1,163 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Site main menu block. + * + * @package block_site_main_menu + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_site_main_menu extends block_list { + function init() { + $this->title = get_string('pluginname', 'block_site_main_menu'); + } + + function applicable_formats() { + return array('site' => true); + } + + function get_content() { + global $USER, $CFG, $DB, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass(); + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + $course = get_site(); + require_once($CFG->dirroot.'/course/lib.php'); + $context = context_course::instance($course->id); + $isediting = $this->page->user_is_editing() && has_capability('moodle/course:manageactivities', $context); + $courserenderer = $this->page->get_renderer('core', 'course'); + +/// extra fast view mode + if (!$isediting) { + $modinfo = get_fast_modinfo($course); + if (!empty($modinfo->sections[0])) { + foreach($modinfo->sections[0] as $cmid) { + $cm = $modinfo->cms[$cmid]; + if (!$cm->uservisible || !$cm->is_visible_on_course_page()) { + continue; + } + + if ($cm->indent > 0) { + $indent = '<div class="mod-indent mod-indent-'.$cm->indent.'"></div>'; + } else { + $indent = ''; + } + + if (!empty($cm->url)) { + $content = html_writer::div($courserenderer->course_section_cm_name($cm), 'activity'); + } else { + $content = $courserenderer->course_section_cm_text($cm); + } + + $this->content->items[] = $indent . html_writer::div($content, 'main-menu-content'); + } + } + return $this->content; + } + + // Slow & hacky editing mode. + $ismoving = ismoving($course->id); + course_create_sections_if_missing($course, 0); + $modinfo = get_fast_modinfo($course); + $section = $modinfo->get_section_info(0); + + if ($ismoving) { + $strmovehere = get_string('movehere'); + $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'")); + $strcancel= get_string('cancel'); + } else { + $strmove = get_string('move'); + } + + if ($ismoving) { + $this->content->icons[] = $OUTPUT->pix_icon('t/move', get_string('move')); + $this->content->items[] = $USER->activitycopyname.' (<a href="'.$CFG->wwwroot.'/course/mod.php?cancelcopy=true&sesskey='.sesskey().'">'.$strcancel.'</a>)'; + } + + if (!empty($modinfo->sections[0])) { + foreach ($modinfo->sections[0] as $modnumber) { + $mod = $modinfo->cms[$modnumber]; + if (!$mod->uservisible || !$mod->is_visible_on_course_page()) { + continue; + } + if (!$ismoving) { + $actions = course_get_cm_edit_actions($mod, $mod->indent); + + // Prepend list of actions with the 'move' action. + $actions = array('move' => new action_menu_link_primary( + new moodle_url('/course/mod.php', array('sesskey' => sesskey(), 'copy' => $mod->id)), + new pix_icon('t/move', $strmove, 'moodle', array('class' => 'iconsmall', 'title' => '')), + $strmove + )) + $actions; + + $editbuttons = html_writer::tag('div', + $courserenderer->course_section_cm_edit_actions($actions, $mod, array('donotenhance' => true)), + array('class' => 'buttons') + ); + } else { + $editbuttons = ''; + } + if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $mod->context)) { + if ($ismoving) { + if ($mod->id == $USER->activitycopy) { + continue; + } + $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?moveto='.$mod->id.'&sesskey='.sesskey().'">'. + '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>'; + $this->content->icons[] = ''; + } + if ($mod->indent > 0) { + $indent = '<div class="mod-indent mod-indent-'.$mod->indent.'"></div>'; + } else { + $indent = ''; + } + if (!$mod->url) { + $content = $courserenderer->course_section_cm_text($mod); + } else { + $content = html_writer::div($courserenderer->course_section_cm_name($mod), ' activity'); + } + $this->content->items[] = $indent . html_writer::div($content . $editbuttons, 'main-menu-content'); + } + } + } + + if ($ismoving) { + $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?movetosection='.$section->id.'&sesskey='.sesskey().'">'. + '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>'; + $this->content->icons[] = ''; + } + + $this->content->footer = $courserenderer->course_section_add_cm_control($course, + 0, null, array('inblock' => true)); + + return $this->content; + } +} + + diff --git a/site_main_menu/classes/privacy/provider.php b/site_main_menu/classes/privacy/provider.php new file mode 100644 index 0000000..b9e6991 --- /dev/null +++ b/site_main_menu/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_site_main_menu. + * + * @package block_site_main_menu + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_site_main_menu\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_site_main_menu implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/site_main_menu/db/access.php b/site_main_menu/db/access.php new file mode 100644 index 0000000..31100e7 --- /dev/null +++ b/site_main_menu/db/access.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/>. + +/** + * Site main menu block caps. + * + * @package block_site_main_menu + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/site_main_menu: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/site_main_menu/lang/en/block_site_main_menu.php b/site_main_menu/lang/en/block_site_main_menu.php new file mode 100644 index 0000000..aba47e3 --- /dev/null +++ b/site_main_menu/lang/en/block_site_main_menu.php @@ -0,0 +1,28 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 'block_site_main_menu', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_site_main_menu + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Main menu'; +$string['site_main_menu:addinstance'] = 'Add a new main menu block'; +$string['privacy:metadata'] = 'The Main menu block only shows data stored in other locations.'; diff --git a/site_main_menu/styles.css b/site_main_menu/styles.css new file mode 100644 index 0000000..9f4747a --- /dev/null +++ b/site_main_menu/styles.css @@ -0,0 +1,34 @@ +.block_site_main_menu li { + clear: both; +} + +.block_site_main_menu.block.list_block .unlist > li > .column { + /* Made specific to win over .block.list_block .unlist > li > .column. */ + width: 100%; + display: table; +} + +.block_site_main_menu li .buttons { + float: right; + margin: 0; + padding: 0; + border: 0; + background-color: inherit; +} + +.block_site_main_menu li .buttons a img { + vertical-align: text-bottom; +} + +.block_site_main_menu .footer { + margin-top: 1em; +} + +.block_site_main_menu .section_add_menus noscript div { + display: inline; +} + +.block_site_main_menu .mod-indent, +.block_site_main_menu .main-menu-content { + display: table-cell; +} diff --git a/site_main_menu/tests/behat/add_url.feature b/site_main_menu/tests/behat/add_url.feature new file mode 100644 index 0000000..a9cfb61 --- /dev/null +++ b/site_main_menu/tests/behat/add_url.feature @@ -0,0 +1,19 @@ +@block @block_site_main_menu +Feature: Add URL to main menu block + In order to add helpful resources for students + As a admin + I need to add URLs to the main menu block and check it works. + + @javascript + Scenario: Add a URL in menu block and ensure it appears + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Main menu" block + When I add a "URL" to section "0" and I fill the form with: + | Name | google | + | Description | gooooooooogle | + | External URL | http://www.google.com | + | id_display | In pop-up | + Then "google" "link" should exist in the "Main menu" "block" + And "Add an activity or resource" "link" should exist in the "Main menu" "block" diff --git a/site_main_menu/tests/behat/behat_block_site_main_menu.php b/site_main_menu/tests/behat/behat_block_site_main_menu.php new file mode 100644 index 0000000..f523116 --- /dev/null +++ b/site_main_menu/tests/behat/behat_block_site_main_menu.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/>. + +/** + * Behat steps definitions for block site main menu + * + * @package block_site_main_menu + * @category test + * @copyright 2016 Marina Glancy + * @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\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\DriverException as DriverException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Behat steps definitions for block site main menu + * + * @package block_site_main_menu + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_site_main_menu extends behat_base { + + /** + * Returns the DOM node of the activity in the site menu block + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $activityname The activity name + * @return NodeElement + */ + protected function get_site_menu_activity_node($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + + return $this->find('xpath', $xpath); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $iconname . '" editing icon', $this->getSession()); + $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_not_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $iconname), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . + '" editing icon when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + + /** + * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. + * + * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block$/ + * @param string $element + * @param string $selectortype + * @param string $activityname + */ + public function i_click_on_in_the_activity_in_site_main_menu_block($element, $selectortype, $activityname) { + $element = $this->get_site_menu_activity_element($element, $selectortype, $activityname); + $element->click(); + } + + /** + * Clicks on the specified element inside the activity container. + * + * @throws ElementNotFoundException + * @param string $element + * @param string $selectortype + * @param string $activityname + * @return NodeElement + */ + protected function get_site_menu_activity_element($element, $selectortype, $activityname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + // Transforming to Behat selector/locator. + list($selector, $locator) = $this->transform_selector($selectortype, $element); + $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . + $selectortype . '" in "' . $activityname . '" '); + + return $this->find($selector, $locator, $exception, $activitynode); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should be hidden$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_hidden($activityname) { + $this->get_site_menu_activity_element("a.dimmed", "css_element", $activityname); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should be available but hidden from course page$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page($activityname) { + $this->get_site_menu_activity_element("a.stealth", "css_element", $activityname); + } + + /** + * Opens an activity actions menu if it is not already opened. + * + * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu in site main menu block$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + */ + public function i_open_actions_menu_in_site_main_menu_block($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); + } +} diff --git a/site_main_menu/tests/behat/edit_activities.feature b/site_main_menu/tests/behat/edit_activities.feature new file mode 100644 index 0000000..c577b9f --- /dev/null +++ b/site_main_menu/tests/behat/edit_activities.feature @@ -0,0 +1,67 @@ +@block @block_site_main_menu +Feature: Edit activities in main menu block + In order to use main menu block + As an admin + I need to add and edit activities there + + @javascript + Scenario: Edit name of acitivity in-place in site main menu block + Given I log in as "admin" + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Main menu" block + When I add a "Forum" to section "0" and I fill the form with: + | Forum name | My forum name | + And I click on "Edit title" "link" in the "My forum name" activity in site main menu block + And I set the field "New name for activity My forum name" to "New forum name" + And I press key "13" in the field "New name for activity My forum name" + Then I should not see "My forum name" + And I should see "New forum name" + And I follow "New forum name" + And I should not see "My forum name" + And I should see "New forum name" + + @javascript + Scenario: Activities in main menu block can be made available but not visible on a course page + And I log in as "admin" + And I set the following administration settings values: + | allowstealth | 1 | + And I am on site homepage + And I navigate to "Turn editing on" node in "Front page settings" + And I add the "Main menu" block + When I add a "Forum" to section "0" and I fill the form with: + | Forum name | Visible forum | + When I add a "Forum" to section "0" and I fill the form with: + | Forum name | My forum name | + And "My forum name" activity in site main menu block should have "Hide" editing icon + And "My forum name" activity in site main menu block should not have "Show" editing icon + And "My forum name" activity in site main menu block should not have "Make available" editing icon + And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in site main menu block + And I click on "Hide" "link" in the "My forum name" activity in site main menu block + And "My forum name" activity in site main menu block should be hidden + And "My forum name" activity in site main menu block should not have "Hide" editing icon + And "My forum name" activity in site main menu block should have "Show" editing icon + And "My forum name" activity in site main menu block should have "Make available" editing icon + And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in site main menu block + And I click on "Make available" "link" in the "My forum name" activity in site main menu block + And "My forum name" activity in site main menu block should be available but hidden from course page + And "My forum name" activity in site main menu block should not have "Hide" editing icon + And "My forum name" activity in site main menu block should have "Show" editing icon + And "My forum name" activity in site main menu block should not have "Make available" editing icon + And "My forum name" activity in site main menu block should have "Make unavailable" editing icon + # Make sure that "Availability" dropdown in the edit menu has three options. + And I open "My forum name" actions menu in site main menu block + And I click on "Edit settings" "link" in the "My forum name" activity in site main menu block + And I expand all fieldsets + And the "Availability" select box should contain "Show on course page" + And the "Availability" select box should contain "Hide from students" + And the field "Availability" matches value "Make available but not shown on course page" + And I press "Save and return to course" + And "My forum name" activity in site main menu block should be available but hidden from course page + And I navigate to "Turn editing off" node in "Front page settings" + And "My forum name" activity in site main menu block should be available but hidden from course page + And I log out + And I should not see "My forum name" in the "Main menu" "block" + And I should see "Visible forum" in the "Main menu" "block" diff --git a/site_main_menu/version.php b/site_main_menu/version.php new file mode 100644 index 0000000..48f164c --- /dev/null +++ b/site_main_menu/version.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/>. + +/** + * Version details + * + * @package block_site_main_menu + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_site_main_menu'; // Full name of the plugin (used for diagnostics) diff --git a/social_activities/block_social_activities.php b/social_activities/block_social_activities.php new file mode 100644 index 0000000..2e5ec90 --- /dev/null +++ b/social_activities/block_social_activities.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/>. + +/** + * Social activities block. + * + * @package block_social_activities + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_social_activities extends block_list { + function init(){ + $this->title = get_string('pluginname', 'block_social_activities'); + } + + function applicable_formats() { + return array('course-view-social' => true); + } + + function get_content() { + global $USER, $CFG, $DB, $OUTPUT; + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass(); + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + if (empty($this->instance)) { + return $this->content; + } + + $course = $this->page->course; + $courserenderer = $this->page->get_renderer('core', 'course'); + + require_once($CFG->dirroot.'/course/lib.php'); + + $context = context_course::instance($course->id); + $isediting = $this->page->user_is_editing() && has_capability('moodle/course:manageactivities', $context); + $modinfo = get_fast_modinfo($course); + +/// extra fast view mode + if (!$isediting) { + if (!empty($modinfo->sections[0])) { + foreach($modinfo->sections[0] as $cmid) { + $cm = $modinfo->cms[$cmid]; + if (!$cm->uservisible || !$cm->is_visible_on_course_page()) { + continue; + } + + if (!$cm->url) { + $content = $courserenderer->course_section_cm_text($cm); + $this->content->items[] = $content; + $this->content->icons[] = ''; + } else { + $this->content->items[] = html_writer::div($courserenderer->course_section_cm_name($cm), 'activity'); + } + } + } + return $this->content; + } + + + // Slow & hacky editing mode. + $ismoving = ismoving($course->id); + $section = $modinfo->get_section_info(0); + + if ($ismoving) { + $strmovehere = get_string('movehere'); + $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'")); + $strcancel= get_string('cancel'); + } else { + $strmove = get_string('move'); + } + + if ($ismoving) { + $this->content->icons[] = ' ' . $OUTPUT->pix_icon('t/move', get_string('move')); + $this->content->items[] = $USER->activitycopyname.' (<a href="'.$CFG->wwwroot.'/course/mod.php?cancelcopy=true&sesskey='.sesskey().'">'.$strcancel.'</a>)'; + } + + if (!empty($modinfo->sections[0])) { + foreach ($modinfo->sections[0] as $modnumber) { + $mod = $modinfo->cms[$modnumber]; + if (!$mod->uservisible || !$mod->is_visible_on_course_page()) { + continue; + } + if (!$ismoving) { + $actions = course_get_cm_edit_actions($mod, -1); + + // Prepend list of actions with the 'move' action. + $actions = array('move' => new action_menu_link_primary( + new moodle_url('/course/mod.php', array('sesskey' => sesskey(), 'copy' => $mod->id)), + new pix_icon('t/move', $strmove, 'moodle', array('class' => 'iconsmall', 'title' => '')), + $strmove + )) + $actions; + + $editbuttons = html_writer::tag('div', + $courserenderer->course_section_cm_edit_actions($actions, $mod, array('donotenhance' => true)), + array('class' => 'buttons') + ); + } else { + $editbuttons = ''; + } + if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $mod->context)) { + if ($ismoving) { + if ($mod->id == $USER->activitycopy) { + continue; + } + $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?moveto='.$mod->id.'&sesskey='.sesskey().'">'. + '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>'; + $this->content->icons[] = ''; + } + if (!$mod->url) { + $content = $courserenderer->course_section_cm_text($mod); + $this->content->items[] = $content . $editbuttons; + $this->content->icons[] = ''; + } else { + $this->content->items[] = html_writer::div($courserenderer->course_section_cm_name($mod), 'activity') . + $editbuttons; + } + } + } + } + + if ($ismoving) { + $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?movetosection='.$section->id.'&sesskey='.sesskey().'">'. + '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>'; + $this->content->icons[] = ''; + } + + $this->content->footer = $courserenderer->course_section_add_cm_control($course, + 0, null, array('inblock' => true)); + + return $this->content; + } +} diff --git a/social_activities/classes/privacy/provider.php b/social_activities/classes/privacy/provider.php new file mode 100644 index 0000000..cad5ba3 --- /dev/null +++ b/social_activities/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_social_activities. + * + * @package block_social_activities + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_social_activities\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_social_activities implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/social_activities/db/access.php b/social_activities/db/access.php new file mode 100644 index 0000000..caa4303 --- /dev/null +++ b/social_activities/db/access.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/>. + +/** + * Social activities block caps. + * + * @package block_social_activities + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/social_activities: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/social_activities/lang/en/block_social_activities.php b/social_activities/lang/en/block_social_activities.php new file mode 100644 index 0000000..3ec6b65 --- /dev/null +++ b/social_activities/lang/en/block_social_activities.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/>. + +/** + * Strings for component 'block_social_activities', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_social_activities + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Social activities'; +$string['social_activities:addinstance'] = 'Add a new social activities block'; +$string['privacy:metadata'] = 'The Social activities block only shows data stored in other locations.'; diff --git a/social_activities/styles.css b/social_activities/styles.css new file mode 100644 index 0000000..14dea69 --- /dev/null +++ b/social_activities/styles.css @@ -0,0 +1,16 @@ +.block_social_activities li { + clear: both; +} + +.block_social_activities li .column { + width: 100%; +} + +.block_social_activities li .buttons { + float: right; + margin: 0; +} + +.block_social_activities li .buttons a img { + vertical-align: text-bottom; +} diff --git a/social_activities/tests/behat/behat_block_social_activities.php b/social_activities/tests/behat/behat_block_social_activities.php new file mode 100644 index 0000000..2fe5526 --- /dev/null +++ b/social_activities/tests/behat/behat_block_social_activities.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/>. + +/** + * Behat steps definitions for block social activities + * + * @package block_social_activities + * @category test + * @copyright 2016 Marina Glancy + * @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\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\DriverException as DriverException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Behat steps definitions for block social activities + * + * @package block_social_activities + * @category test + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_social_activities extends behat_base { + + /** + * Returns the DOM node of the activity in the social activities block + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $activityname The activity name + * @return NodeElement + */ + protected function get_social_block_activity_node($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]"; + + return $this->find('xpath', $xpath); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_social_activities_block_should_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $iconname . '" editing icon', $this->getSession()); + $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_social_activities_block_should_not_have_editing_icon($activityname, $iconname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $iconname), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . + '" editing icon when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + + /** + * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. + * + * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block$/ + * @param string $element + * @param string $selectortype + * @param string $activityname + */ + public function i_click_on_in_the_activity_in_social_activities_block($element, $selectortype, $activityname) { + $element = $this->get_social_block_activity_element($element, $selectortype, $activityname); + $element->click(); + } + + /** + * Clicks on the specified element inside the activity container. + * + * @throws ElementNotFoundException + * @param string $element + * @param string $selectortype + * @param string $activityname + * @return NodeElement + */ + protected function get_social_block_activity_element($element, $selectortype, $activityname) { + $activitynode = $this->get_social_block_activity_node($activityname); + + // Transforming to Behat selector/locator. + list($selector, $locator) = $this->transform_selector($selectortype, $element); + $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . + $selectortype . '" in "' . $activityname . '" '); + + return $this->find($selector, $locator, $exception, $activitynode); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should be hidden$/ + * @param string $activityname + */ + public function activity_in_social_activities_block_should_be_hidden($activityname) { + $this->get_social_block_activity_element("a.dimmed", "css_element", $activityname); + } + + /** + * Checks that the specified activity is hidden. + * + * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should be available but hidden from course page$/ + * @param string $activityname + */ + public function activity_in_social_activities_block_should_be_available_but_hidden_from_course_page($activityname) { + $this->get_social_block_activity_element("a.stealth", "css_element", $activityname); + } + + /** + * Opens an activity actions menu if it is not already opened. + * + * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu in social activities block$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + */ + public function i_open_actions_menu_in_social_activities_block($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]"; + $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); + } +} diff --git a/social_activities/tests/behat/edit_activities.feature b/social_activities/tests/behat/edit_activities.feature new file mode 100644 index 0000000..ad67635 --- /dev/null +++ b/social_activities/tests/behat/edit_activities.feature @@ -0,0 +1,85 @@ +@block @block_social_activities @format_social +Feature: Edit activities in social activities block + In order to use social activities block + As a teacher + I need to add and edit activities there + + Background: + Given the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | social | + And the following "users" exist: + | username | firstname | lastname | + | user1 | User | One | + | student1 | Student | One | + And the following "course enrolments" exist: + | user | course | role | + | user1 | C1 | editingteacher | + | student1 | C1 | student | + + @javascript + Scenario: Edit name of acitivity in-place in social activities block + Given I log in as "user1" + And I am on "Course 1" course homepage with editing mode on + And I set the field "Add an activity to section 'section 0'" to "Forum" + And I set the field "Forum name" to "My forum name" + And I press "Save and return to course" + And I click on "Edit title" "link" in the "My forum name" activity in social activities block + And I set the field "New name for activity My forum name" to "New forum name" + And I press key "13" in the field "New name for activity My forum name" + Then I should not see "My forum name" in the "Social activities" "block" + And I should see "New forum name" + And I follow "New forum name" + And I should not see "My forum name" + And I should see "New forum name" + + @javascript + Scenario: Activities in social activities block can be made available but not visible on a course page + And I log in as "admin" + And I set the following administration settings values: + | allowstealth | 1 | + And I log out + And I log in as "user1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Recent activity" block + And I set the field "Add an activity to section 'section 0'" to "Forum" + And I set the field "Forum name" to "My forum name" + And I press "Save and return to course" + And "My forum name" activity in social activities block should have "Hide" editing icon + And "My forum name" activity in social activities block should not have "Show" editing icon + And "My forum name" activity in social activities block should not have "Make available" editing icon + And "My forum name" activity in social activities block should not have "Make unavailable" editing icon + And I wait until the page is ready + And I open "My forum name" actions menu in social activities block + And I click on "Hide" "link" in the "My forum name" activity in social activities block + And "My forum name" activity in social activities block should be hidden + And "My forum name" activity in social activities block should not have "Hide" editing icon + And "My forum name" activity in social activities block should have "Show" editing icon + And "My forum name" activity in social activities block should have "Make available" editing icon + And "My forum name" activity in social activities block should not have "Make unavailable" editing icon + And I open "My forum name" actions menu in social activities block + And I click on "Make available" "link" in the "My forum name" activity in social activities block + And "My forum name" activity in social activities block should be available but hidden from course page + And "My forum name" activity in social activities block should not have "Hide" editing icon + And "My forum name" activity in social activities block should have "Show" editing icon + And "My forum name" activity in social activities block should not have "Make available" editing icon + And "My forum name" activity in social activities block should have "Make unavailable" editing icon + # Make sure that "Availability" dropdown in the edit menu has three options. + And I open "My forum name" actions menu in social activities block + And I click on "Edit settings" "link" in the "My forum name" activity in social activities block + And I expand all fieldsets + And the "Availability" select box should contain "Show on course page" + And the "Availability" select box should contain "Hide from students" + And the field "Availability" matches value "Make available but not shown on course page" + And I press "Save and return to course" + And "My forum name" activity in social activities block should be available but hidden from course page + And I turn editing mode off + And "My forum name" activity in social activities block should be available but hidden from course page + And I log out + # Student will not see the module on the course page but can access it from other reports and blocks: + And I log in as "student1" + And I am on "Course 1" course homepage + And I should not see "My forum name" in the "Social activities" "block" + And I click on "My forum name" "link" in the "Recent activity" "block" + And I should see "My forum name" in the ".breadcrumb" "css_element" + And I log out diff --git a/social_activities/version.php b/social_activities/version.php new file mode 100644 index 0000000..491fc5f --- /dev/null +++ b/social_activities/version.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/>. + +/** + * Version details + * + * @package block_social_activities + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_social_activities'; // Full name of the plugin (used for diagnostics) diff --git a/tag_flickr/block_tag_flickr.php b/tag_flickr/block_tag_flickr.php new file mode 100644 index 0000000..22471f3 --- /dev/null +++ b/tag_flickr/block_tag_flickr.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/>. + +/** + * Flickr tag block. + * + * @package block_tag_flickr + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('FLICKR_DEV_KEY', '4fddbdd7ff2376beec54d7f6afad425e'); +define('DEFAULT_NUMBER_OF_PHOTOS', 6); + +class block_tag_flickr extends block_base { + + function init() { + $this->title = get_string('pluginname','block_tag_flickr'); + } + + function applicable_formats() { + return array('tag' => true); + } + + function specialization() { + $this->title = !empty($this->config->title) ? $this->config->title : get_string('pluginname', 'block_tag_flickr'); + } + + function instance_allow_multiple() { + return true; + } + + function get_content() { + global $CFG, $USER; + + //note: do NOT include files at the top of this file + require_once($CFG->libdir . '/filelib.php'); + + if ($this->content !== NULL) { + return $this->content; + } + + $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility + $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. + + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); + } + + if (empty($tagobject)) { + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + return $this->content; + } + + //include related tags in the photo query ? + $tagscsv = $tagobject->name; + if (!empty($this->config->includerelatedtags)) { + foreach ($tagobject->get_related_tags() as $t) { + $tagscsv .= ',' . $t->get_display_name(false); + } + } + $tagscsv = urlencode($tagscsv); + + //number of photos to display + $numberofphotos = DEFAULT_NUMBER_OF_PHOTOS; + if( !empty($this->config->numberofphotos)) { + $numberofphotos = $this->config->numberofphotos; + } + + //sort search results by + $sortby = 'relevance'; + if( !empty($this->config->sortby)) { + $sortby = $this->config->sortby; + } + + //pull photos from a specific photoset + if(!empty($this->config->photoset)){ + + $request = 'https://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos'; + $request .= '&api_key='.FLICKR_DEV_KEY; + $request .= '&photoset_id='.$this->config->photoset; + $request .= '&per_page='.$numberofphotos; + $request .= '&format=php_serial'; + + $response = $this->fetch_request($request); + + $search = unserialize($response); + + foreach ($search['photoset']['photo'] as $p){ + $p['owner'] = $search['photoset']['owner']; + } + + $photos = array_values($search['photoset']['photo']); + + } + //search for photos tagged with $tagscsv + else{ + + $request = 'https://api.flickr.com/services/rest/?method=flickr.photos.search'; + $request .= '&api_key='.FLICKR_DEV_KEY; + $request .= '&tags='.$tagscsv; + $request .= '&per_page='.$numberofphotos; + $request .= '&sort='.$sortby; + $request .= '&format=php_serial'; + + $response = $this->fetch_request($request); + + $search = unserialize($response); + $photos = array_values($search['photos']['photo']); + } + + + if(strcmp($search['stat'], 'ok') != 0) return; //if no results were returned, exit... + + //Accessibility: render the list of photos + $text = '<ul class="inline-list">'; + foreach ($photos as $photo) { + $text .= '<li><a href="http://www.flickr.com/photos/' . $photo['owner'] . '/' . $photo['id'] . '/" title="'.s($photo['title']).'">'; + $text .= '<img alt="'.s($photo['title']).'" class="flickr-photos" src="'. $this->build_photo_url($photo, 'square') ."\" /></a></li>\n"; + } + $text .= "</ul>\n"; + + $this->content = new stdClass; + $this->content->text = $text; + $this->content->footer = ''; + + return $this->content; + } + + function fetch_request($request){ + $c = new curl(array('cache' => true, 'module_cache'=> 'tag_flickr')); + + $response = $c->get($request); + + return $response; + } + + function build_photo_url ($photo, $size='medium') { + //receives an array (can use the individual photo data returned + //from an API call) and returns a URL (doesn't mean that the + //file size exists) + $sizes = array( + 'square' => '_s', + 'thumbnail' => '_t', + 'small' => '_m', + 'medium' => '', + 'large' => '_b', + 'original' => '_o' + ); + + $size = strtolower($size); + if (!array_key_exists($size, $sizes)) { + $size = 'medium'; + } + + if ($size == 'original') { + $url = 'http://farm' . $photo['farm'] . '.static.flickr.com/' . $photo['server'] . '/' . $photo['id'] . '_' . $photo['originalsecret'] . '_o' . '.' . $photo['originalformat']; + } else { + $url = 'http://farm' . $photo['farm'] . '.static.flickr.com/' . $photo['server'] . '/' . $photo['id'] . '_' . $photo['secret'] . $sizes[$size] . '.jpg'; + } + return $url; + } +} + + diff --git a/tag_flickr/classes/privacy/provider.php b/tag_flickr/classes/privacy/provider.php new file mode 100644 index 0000000..fb3673d --- /dev/null +++ b/tag_flickr/classes/privacy/provider.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/>. + +/** + * Privacy Subsystem implementation for block_tag_flickr. + * + * @package block_tag_flickr + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_tag_flickr\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\context; +use core_privacy\local\request\contextlist; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_tag_flickr implementing metadata and plugin provider. + * + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { + + /** + * 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_external_location_link( + 'flickr.com', + [ + 'tags' => 'privacy:metadata:block_tag_flickr:tags' + ], + 'privacy:metadata:block_tag_flickr' + ); + + 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 contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + return new contextlist(); + } + + /** + * 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) { + } + + /** + * 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) { + } + + /** + * 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) { + } + +} diff --git a/tag_flickr/db/access.php b/tag_flickr/db/access.php new file mode 100644 index 0000000..2e38dec --- /dev/null +++ b/tag_flickr/db/access.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/>. + +/** + * Tag flickr block caps. + * + * @package block_tag_flickr + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/tag_flickr: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/tag_flickr/edit_form.php b/tag_flickr/edit_form.php new file mode 100644 index 0000000..da0a23f --- /dev/null +++ b/tag_flickr/edit_form.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/>. + +/** + * Form for editing tag_flickr block instances. + * + * @package block_tag_flickr + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing tag_flickr block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_tag_flickr_edit_form extends block_edit_form { + protected function specific_definition($mform) { + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitle', 'block_tag_flickr')); + $mform->setType('config_title', PARAM_TEXT); + + $mform->addElement('text', 'config_numberofphotos', get_string('numberofphotos', 'block_tag_flickr'), array('size' => 5)); + $mform->setType('config_numberofphotos', PARAM_INT); + + $mform->addElement('selectyesno', 'config_includerelatedtags', get_string('includerelatedtags', 'block_tag_flickr')); + $mform->setDefault('config_includerelatedtags', 0); + + $sortoptions = array( + 'date-posted-asc' => get_string('date-posted-asc', 'block_tag_flickr'), + 'date-posted-desc' => get_string('date-posted-desc', 'block_tag_flickr'), + 'date-taken-asc' => get_string('date-taken-asc', 'block_tag_flickr'), + 'date-taken-desc' => get_string('date-taken-desc', 'block_tag_flickr'), + 'interestingness-asc' => get_string('interestingness-asc', 'block_tag_flickr'), + 'interestingness-desc' => get_string('interestingness-desc', 'block_tag_flickr'), + 'relevance' => get_string('relevance', 'block_tag_flickr'), + ); + $mform->addElement('select', 'config_sortby', get_string('sortby', 'block_tag_flickr'), $sortoptions); + $mform->setDefault('config_sortby', 'relevance'); + + $mform->addElement('text', 'config_photoset', get_string('getfromphotoset', 'block_tag_flickr')); + $mform->setType('config_photoset', PARAM_ALPHANUM); + } +} diff --git a/tag_flickr/lang/en/block_tag_flickr.php b/tag_flickr/lang/en/block_tag_flickr.php new file mode 100644 index 0000000..637d015 --- /dev/null +++ b/tag_flickr/lang/en/block_tag_flickr.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/>. + +/** + * Strings for component 'block_tag_flickr', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_tag_flickr + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['configtitle'] = 'Flickr block title'; +$string['date-posted-asc'] = 'Date posted ASC'; +$string['date-posted-desc'] = 'Date posted DESC'; +$string['date-taken-asc'] = 'Date taken ASC'; +$string['date-taken-desc'] = 'Date taken DESC'; +$string['defaulttile'] = 'Flickr'; +$string['getfromphotoset'] = 'Get photos from photoset with id'; +$string['includerelatedtags'] = 'Include related tags in query'; +$string['interestingness-asc'] = 'Interestingness ASC'; +$string['interestingness-desc'] = 'Interestingness DESC'; +$string['numberofphotos'] = 'Number of photos'; +$string['pluginname'] = 'Flickr'; +$string['relevance'] = 'Relevance'; +$string['sortby'] = 'Sort by'; +$string['tag_flickr:addinstance'] = 'Add a new flickr block'; +$string['privacy:metadata:block_tag_flickr'] = 'The Flickr block plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; +$string['privacy:metadata:block_tag_flickr:tags'] = 'The tag values sent as CSV format to search for Flickr images.'; diff --git a/tag_flickr/styles.css b/tag_flickr/styles.css new file mode 100644 index 0000000..08a9a60 --- /dev/null +++ b/tag_flickr/styles.css @@ -0,0 +1,3 @@ +.block_tag_flickr .flickr-photos { + padding: 3px; +} \ No newline at end of file diff --git a/tag_flickr/tests/behat/configuring_tag_flickr_block.feature b/tag_flickr/tests/behat/configuring_tag_flickr_block.feature new file mode 100644 index 0000000..f213f66 --- /dev/null +++ b/tag_flickr/tests/behat/configuring_tag_flickr_block.feature @@ -0,0 +1,20 @@ +@block @block_tag_flickr +Feature: Adding and configuring Flickr block + In order to have the Flickr block used + As a admin + I need to add the Flickr block to the tags site page + + @javascript + Scenario: Adding Flickr block to the tags site page + Given I log in as "admin" + And I press "Customise this page" + # TODO MDL-57120 site "Tags" link not accessible without navigation block. + And I add the "Navigation" block if not present + And I navigate to "Tags" node in "Site pages" + And I add the "Flickr" block + And I configure the "Flickr" block + Then I should see "Flickr block title" + And I set the field "Flickr block title" to "The Flickr block header" + And I press "Save changes" + And "block_tag_flickr" "block" should exist + Then "The Flickr block header" "block" should exist diff --git a/tag_flickr/version.php b/tag_flickr/version.php new file mode 100644 index 0000000..36247e3 --- /dev/null +++ b/tag_flickr/version.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/>. + +/** + * Version details + * + * @package block_tag_flickr + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_tag_flickr'; // Full name of the plugin (used for diagnostics) diff --git a/tag_youtube/block_tag_youtube.php b/tag_youtube/block_tag_youtube.php new file mode 100644 index 0000000..9c79fe8 --- /dev/null +++ b/tag_youtube/block_tag_youtube.php @@ -0,0 +1,402 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Tag youtube block + * + * @package block_tag_youtube + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('DEFAULT_NUMBER_OF_VIDEOS', 5); + +class block_tag_youtube extends block_base { + + /** + * @var Google_Service_Youtube + */ + protected $service = null; + + function init() { + $this->title = get_string('pluginname','block_tag_youtube'); + $this->config = new stdClass(); + } + + function applicable_formats() { + return array('tag' => true); + } + + /** + * It can be configured. + * + * @return bool + */ + public function has_config() { + return true; + } + + function specialization() { + $this->title = !empty($this->config->title) ? $this->config->title : get_string('pluginname', 'block_tag_youtube'); + // Convert numeric categories (old YouTube API) to + // textual ones (new Google Data API) + $this->config->category = !empty($this->config->category) ? $this->category_map_old2new($this->config->category) : '0'; + } + + function instance_allow_multiple() { + return true; + } + + function get_content() { + global $CFG; + + //note: do NOT include files at the top of this file + require_once($CFG->libdir . '/filelib.php'); + + if ($this->content !== NULL) { + return $this->content; + } + + $this->content = new stdClass(); + $this->content->footer = ''; + + if (!$this->get_service()) { + $this->content->text = $this->get_error_message(); + return $this->content; + } + + $text = ''; + if(!empty($this->config->playlist)){ + //videos from a playlist + $text = $this->get_videos_by_playlist(); + } + else{ + if(!empty($this->config->category)){ + //videos from category with tag + $text = $this->get_videos_by_tag_and_category(); + } + else { + //videos with tag + $text = $this->get_videos_by_tag(); + } + } + + $this->content->text = $text; + + return $this->content; + } + + function get_videos_by_playlist(){ + + if (!$service = $this->get_service()) { + return $this->get_error_message(); + } + + $numberofvideos = DEFAULT_NUMBER_OF_VIDEOS; + if( !empty($this->config->numberofvideos)) { + $numberofvideos = $this->config->numberofvideos; + } + + try { + $response = $service->playlistItems->listPlaylistItems('id,snippet', array( + 'playlistId' => $this->config->playlist, + 'maxResults' => $numberofvideos + )); + } catch (Google_Service_Exception $e) { + debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER); + return $this->get_error_message(get_string('requesterror', 'block_tag_youtube')); + } + + return $this->render_items($response); + } + + function get_videos_by_tag(){ + + if (!$service = $this->get_service()) { + return $this->get_error_message(); + } + + $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility + $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. + + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); + } + + if (empty($tagobject)) { + return ''; + } + + $querytag = urlencode($tagobject->name); + + $numberofvideos = DEFAULT_NUMBER_OF_VIDEOS; + if ( !empty($this->config->numberofvideos) ) { + $numberofvideos = $this->config->numberofvideos; + } + + try { + $response = $service->search->listSearch('id,snippet', array( + 'q' => $querytag, + 'type' => 'video', + 'maxResults' => $numberofvideos + )); + } catch (Google_Service_Exception $e) { + debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER); + return $this->get_error_message(get_string('requesterror', 'block_tag_youtube')); + } + + return $this->render_items($response); + } + + function get_videos_by_tag_and_category(){ + + if (!$service = $this->get_service()) { + return $this->get_error_message(); + } + + $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility + $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. + + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); + } + + if (empty($tagobject)) { + return ''; + } + + $querytag = urlencode($tagobject->name); + + $numberofvideos = DEFAULT_NUMBER_OF_VIDEOS; + if( !empty($this->config->numberofvideos)) { + $numberofvideos = $this->config->numberofvideos; + } + + try { + $response = $service->search->listSearch('id,snippet', array( + 'q' => $querytag, + 'type' => 'video', + 'maxResults' => $numberofvideos, + 'videoCategoryId' => $this->config->category + )); + } catch (Google_Service_Exception $e) { + debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER); + return $this->get_error_message(get_string('requesterror', 'block_tag_youtube')); + } + + return $this->render_items($response); + } + + /** + * Sends a request to fetch data. + * + * @see block_tag_youtube::service + * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more. + * @param string $request + * @throws coding_exception + */ + public function fetch_request($request) { + throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::get_service instead.'); + + $c = new curl(array('cache' => true, 'module_cache'=>'tag_youtube')); + $c->setopt(array('CURLOPT_TIMEOUT' => 3, 'CURLOPT_CONNECTTIMEOUT' => 3)); + + $response = $c->get($request); + + $xml = new SimpleXMLElement($response); + return $this->render_video_list($xml); + } + + /** + * Renders the video list. + * + * @see block_tag_youtube::render_items + * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more. + * @param SimpleXMLElement $xml + * @throws coding_exception + */ + function render_video_list(SimpleXMLElement $xml){ + throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::render_items instead.'); + } + + /** + * Returns an error message. + * + * Useful when the block is not properly set or something goes wrong. + * + * @param string $message The message to display. + * @return string HTML + */ + protected function get_error_message($message = null) { + global $OUTPUT; + + if (empty($message)) { + $message = get_string('apierror', 'block_tag_youtube'); + } + return $OUTPUT->notification($message); + } + + /** + * Gets the youtube service object. + * + * @return Google_Service_YouTube + */ + protected function get_service() { + global $CFG; + + if (!$apikey = get_config('block_tag_youtube', 'apikey')) { + return false; + } + + // Wrapped in an if in case we call different get_videos_* multiple times. + if (!isset($this->service)) { + require_once($CFG->libdir . '/google/lib.php'); + $client = get_google_client(); + $client->setDeveloperKey($apikey); + $client->setScopes(array(Google_Service_YouTube::YOUTUBE_READONLY)); + $this->service = new Google_Service_YouTube($client); + } + + return $this->service; + } + + /** + * Renders the list of items. + * + * @param array $videosdata + * @return string HTML + */ + protected function render_items($videosdata) { + + if (!$videosdata || empty($videosdata->items)) { + if (!empty($videosdata->error)) { + debugging('Error fetching data from youtube: ' . $videosdata->error->message, DEBUG_DEVELOPER); + } + return ''; + } + + // If we reach that point we already know that the API key is set. + $service = $this->get_service(); + + $text = html_writer::start_tag('ul', array('class' => 'yt-video-entry unlist img-text')); + foreach ($videosdata->items as $video) { + + // Link to the video included in the playlist if listing a playlist. + if (!empty($video->snippet->resourceId)) { + $id = $video->snippet->resourceId->videoId; + $playlist = '&list=' . $video->snippet->playlistId; + } else { + $id = $video->id->videoId; + $playlist = ''; + } + + $thumbnail = $video->snippet->getThumbnails()->getDefault(); + $url = 'http://www.youtube.com/watch?v=' . $id . $playlist; + + $videodetails = $service->videos->listVideos('id,contentDetails', array('id' => $id)); + if ($videodetails && !empty($videodetails->items)) { + + // We fetch by id so we just use the first one. + $details = $videodetails->items[0]; + $start = new DateTime('@0'); + $start->add(new DateInterval($details->contentDetails->duration)); + $seconds = $start->format('U'); + } + + $text .= html_writer::start_tag('li'); + + $imgattrs = array('class' => 'youtube-thumb', 'src' => $thumbnail->url, 'alt' => $video->snippet->title); + $thumbhtml = html_writer::empty_tag('img', $imgattrs); + $link = html_writer::tag('a', $thumbhtml, array('href' => $url)); + $text .= html_writer::tag('div', $link, array('class' => 'clearfix')); + + $text .= html_writer::tag('span', html_writer::tag('a', $video->snippet->title, array('href' => $url))); + + if (!empty($seconds)) { + $text .= html_writer::tag('div', format_time($seconds)); + } + $text .= html_writer::end_tag('li'); + } + $text .= html_writer::end_tag('ul'); + + return $text; + } + + function get_categories() { + // TODO: Right now using sticky categories from + // http://gdata.youtube.com/schemas/2007/categories.cat + // This should be performed from time to time by the block insead + // and cached somewhere, avoiding deprecated ones and observing regions + return array ( + '0' => get_string('anycategory', 'block_tag_youtube'), + 'Film' => get_string('filmsanimation', 'block_tag_youtube'), + 'Autos' => get_string('autosvehicles', 'block_tag_youtube'), + 'Music' => get_string('music', 'block_tag_youtube'), + 'Animals'=> get_string('petsanimals', 'block_tag_youtube'), + 'Sports' => get_string('sports', 'block_tag_youtube'), + 'Travel' => get_string('travel', 'block_tag_youtube'), + 'Games' => get_string('gadgetsgames', 'block_tag_youtube'), + 'Comedy' => get_string('comedy', 'block_tag_youtube'), + 'People' => get_string('peopleblogs', 'block_tag_youtube'), + 'News' => get_string('newspolitics', 'block_tag_youtube'), + 'Entertainment' => get_string('entertainment', 'block_tag_youtube'), + 'Education' => get_string('education', 'block_tag_youtube'), + 'Howto' => get_string('howtodiy', 'block_tag_youtube'), + 'Tech' => get_string('scienceandtech', 'block_tag_youtube') + ); + } + + /** + * Provide conversion from old numeric categories available in youtube API + * to the new ones available in the Google API + * + * @param int $oldcat old category code + * @return mixed new category code or 0 (if no match found) + * + * TODO: Someday this should be applied on upgrade for all the existing + * block instances so we won't need the mapping any more. That would imply + * to implement restore handling to perform the conversion of old blocks. + */ + function category_map_old2new($oldcat) { + $oldoptions = array ( + 0 => '0', + 1 => 'Film', + 2 => 'Autos', + 23 => 'Comedy', + 24 => 'Entertainment', + 10 => 'Music', + 25 => 'News', + 22 => 'People', + 15 => 'Animals', + 26 => 'Howto', + 17 => 'Sports', + 19 => 'Travel', + 20 => 'Games' + ); + if (array_key_exists($oldcat, $oldoptions)) { + return $oldoptions[$oldcat]; + } else { + return $oldcat; + } + } +} + diff --git a/tag_youtube/classes/privacy/provider.php b/tag_youtube/classes/privacy/provider.php new file mode 100644 index 0000000..e4b1c08 --- /dev/null +++ b/tag_youtube/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_tag_youtube. + * + * @package block_tag_youtube + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_tag_youtube\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_tag_youtube implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/tag_youtube/db/access.php b/tag_youtube/db/access.php new file mode 100644 index 0000000..36eee9e --- /dev/null +++ b/tag_youtube/db/access.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/>. + +/** + * Tag youtube block caps. + * + * @package block_tag_youtube + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/tag_youtube: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/tag_youtube/db/install.php b/tag_youtube/db/install.php new file mode 100644 index 0000000..0572762 --- /dev/null +++ b/tag_youtube/db/install.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/>. + +/** + * Tag Youtube block installation. + * + * @package block_tag_youtube + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Sets the install values for the tag_youtube entry in the block table. + * + * @return void + */ +function xmldb_block_tag_youtube_install() { + global $DB; + + // Disable this block by default. + $DB->set_field('block', 'visible', 0, array('name' => 'tag_youtube')); +} + diff --git a/tag_youtube/edit_form.php b/tag_youtube/edit_form.php new file mode 100644 index 0000000..92b54d9 --- /dev/null +++ b/tag_youtube/edit_form.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/>. + +/** + * Form for editing tag_youtube block instances. + * + * @package block_tag_youtube + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing tag_youtube block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_tag_youtube_edit_form extends block_edit_form { + protected function specific_definition($mform) { + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitle', 'block_tag_youtube')); + $mform->setType('config_title', PARAM_TEXT); + + $mform->addElement('text', 'config_numberofvideos', get_string('numberofvideos', 'block_tag_youtube'), array('size' => 5)); + $mform->setType('config_numberofvideos', PARAM_INT); + + $categorychoices = $this->block->get_categories(); + $mform->addElement('select', 'config_category', get_string('category', 'block_tag_youtube'), $categorychoices); + $mform->setDefault('config_category', 0); + + $mform->addElement('text', 'config_playlist', get_string('includeonlyvideosfromplaylist', 'block_tag_youtube')); + $mform->setType('config_playlist', PARAM_ALPHANUM); + } +} diff --git a/tag_youtube/lang/en/block_tag_youtube.php b/tag_youtube/lang/en/block_tag_youtube.php new file mode 100644 index 0000000..05ecc00 --- /dev/null +++ b/tag_youtube/lang/en/block_tag_youtube.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/>. + +/** + * Strings for component 'block_tag_youtube', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_tag_youtube + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['anycategory'] = 'Any category'; +$string['apierror'] = 'The YouTube API key is not set. Contact your administrator.'; +$string['apikey'] = 'API key'; +$string['apikeyinfo'] = 'Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API key</a> for your Moodle site.'; +$string['autosvehicles'] = 'Autos & Vehicles'; +$string['category'] = 'Category'; +$string['comedy'] = 'Comedy'; +$string['configtitle'] = 'YouTube block title'; +$string['education'] = 'Education'; +$string['entertainment'] = 'Entertainment'; +$string['filmsanimation'] = 'Films & Animation'; +$string['gadgetsgames'] = 'Gadgets & Games'; +$string['howtodiy'] = 'How-to & DIY'; +$string['includeonlyvideosfromplaylist'] = 'Include only videos from the playlist with id'; +$string['music'] = 'Music'; +$string['newspolitics'] = 'News & Politics'; +$string['numberofvideos'] = 'Number of videos'; +$string['peopleblogs'] = 'People & Blogs'; +$string['petsanimals'] = 'Pets & Animals'; +$string['pluginname'] = 'YouTube'; +$string['requesterror'] = 'Data could not be obtained from the server. Contact your administrator if the problem persists.'; +$string['scienceandtech'] = 'Science & Tech'; +$string['sports'] = 'Sports'; +$string['tag_youtube:addinstance'] = 'Add a new YouTube block'; +$string['travel'] = 'Travel & Places'; +$string['privacy:metadata'] = 'The YouTube block only shows data stored in other locations.'; diff --git a/tag_youtube/settings.php b/tag_youtube/settings.php new file mode 100644 index 0000000..ad9f443 --- /dev/null +++ b/tag_youtube/settings.php @@ -0,0 +1,30 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more 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 the RSS client block. + * + * @package block_tag_youtube + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtext('block_tag_youtube/apikey', get_string('apikey', 'block_tag_youtube'), + get_string('apikeyinfo', 'block_tag_youtube'), '', PARAM_RAW_TRIMMED, 40)); +} diff --git a/tag_youtube/styles.css b/tag_youtube/styles.css new file mode 100644 index 0000000..0956ff3 --- /dev/null +++ b/tag_youtube/styles.css @@ -0,0 +1,10 @@ +.block_tag_youtube .youtube-thumb { + padding: 3px; + padding-bottom: 0.5em; + display: block; + float: left; +} + +.block_tag_youtube .yt-video-entry li { + clear: left; +} \ No newline at end of file diff --git a/tag_youtube/tests/block_tag_youtube_test.php b/tag_youtube/tests/block_tag_youtube_test.php new file mode 100644 index 0000000..6434c44 --- /dev/null +++ b/tag_youtube/tests/block_tag_youtube_test.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/>. + +/** + * Block Tag Youtube tests + * + * @package block_tag_youtube + * @category test + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Block Tag Youtube test class. + * + * @package block_tag_youtube + * @category test + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_block_tag_youtube_testcase extends advanced_testcase { + + /** + * Testing the tag youtube block's initial state after a new installation. + * + * @return void + */ + public function test_after_install() { + global $DB; + + $this->resetAfterTest(true); + + // Assert that tag_youtube entry exists and that its visible attribute is set to 0 (disabled). + $this->assertTrue($DB->record_exists('block', array('name' => 'tag_youtube', 'visible' => 0))); + } +} diff --git a/tag_youtube/upgrade.txt b/tag_youtube/upgrade.txt new file mode 100644 index 0000000..ae3d80d --- /dev/null +++ b/tag_youtube/upgrade.txt @@ -0,0 +1,8 @@ +This files describes API changes in the block tag_youtube code. + +=== 3.0 === + +* Due to the final YouTube API v2.0 deprecation we needed to adapt the current + code to YouTube Data API v3. block_tag_youtube::fetch_request and + block_tag_youtube::render_video_list have been deprecated as they can not be + used any more. diff --git a/tag_youtube/version.php b/tag_youtube/version.php new file mode 100644 index 0000000..ae3bfef --- /dev/null +++ b/tag_youtube/version.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/>. + +/** + * Version details + * + * @package block_tag_youtube + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_tag_youtube'; // Full name of the plugin (used for diagnostics) diff --git a/tags/backup/moodle2/restore_tags_block_task.class.php b/tags/backup/moodle2/restore_tags_block_task.class.php new file mode 100644 index 0000000..11d2087 --- /dev/null +++ b/tags/backup/moodle2/restore_tags_block_task.class.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/>. + +/** + * @package block_tags + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Specialised restore task for the tags block + * (using execute_after_tasks for recoding of tag collection id) + * + * @package block_tags + * @copyright 2016 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_tags_block_task extends restore_block_task { + + protected function define_my_settings() { + } + + protected function define_my_steps() { + } + + public function get_fileareas() { + return array(); // No associated fileareas. + } + + public function get_configdata_encoded_attributes() { + return array(); // No special handling of configdata. + } + + /** + * This function, executed after all the tasks in the plan + * have been executed, will remove tag collection reference in case block was restored into another site. + * Also get mapping of contextid. + */ + public function after_restore() { + global $DB; + + // Get the blockid. + $blockid = $this->get_blockid(); + + // Extract block configdata and remove tag collection reference if this is another site. Also map contextid. + if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) { + $config = unserialize(base64_decode($configdata)); + $changed = false; + if (!empty($config->tagcoll) && $config->tagcoll > 1 && !$this->is_samesite()) { + $config->tagcoll = 0; + $changed = true; + } + if (!empty($config->ctx)) { + if ($ctxmap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $config->ctx)) { + $config->ctx = $ctxmap->newitemid; + } else { + $config->ctx = 0; + } + $changed = true; + } + if ($changed) { + $configdata = base64_encode(serialize($config)); + $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid)); + } + } + } + + static public function define_decode_contents() { + return array(); + } + + static public function define_decode_rules() { + return array(); + } +} diff --git a/tags/block_tags.php b/tags/block_tags.php new file mode 100644 index 0000000..f263ddf --- /dev/null +++ b/tags/block_tags.php @@ -0,0 +1,112 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Tags block. + * + * @package block_tags + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class block_tags extends block_base { + public function init() { + $this->title = get_string('pluginname', 'block_tags'); + } + + public function instance_allow_multiple() { + return true; + } + + public function applicable_formats() { + return array('all' => true); + } + + public function instance_allow_config() { + return true; + } + + public function specialization() { + + // Load userdefined title and make sure it's never empty. + if (empty($this->config->title)) { + $this->title = get_string('pluginname', 'block_tags'); + } else { + $this->title = format_string($this->config->title, true, ['context' => $this->context]); + } + } + + public function get_content() { + + global $CFG, $COURSE, $USER, $SCRIPT, $OUTPUT; + + if (empty($CFG->usetags)) { + $this->content = new stdClass(); + $this->content->text = ''; + if ($this->page->user_is_editing()) { + $this->content->text = get_string('disabledtags', 'block_tags'); + } + return $this->content; + } + + if (!isset($this->config)) { + $this->config = new stdClass(); + } + + if (empty($this->config->numberoftags)) { + $this->config->numberoftags = 80; + } + + if (empty($this->config->showstandard)) { + $this->config->showstandard = core_tag_tag::BOTH_STANDARD_AND_NOT; + } + + if (empty($this->config->ctx)) { + $this->config->ctx = 0; + } + + if (empty($this->config->rec)) { + $this->config->rec = 1; + } + + if (empty($this->config->tagcoll)) { + $this->config->tagcoll = 0; + } + + if ($this->content !== NULL) { + return $this->content; + } + + if (empty($this->instance)) { + $this->content = ''; + return $this->content; + } + + $this->content = new stdClass; + $this->content->text = ''; + $this->content->footer = ''; + + // Get a list of tags. + + $tagcloud = core_tag_collection::get_tag_cloud($this->config->tagcoll, + $this->config->showstandard == core_tag_tag::STANDARD_ONLY, + $this->config->numberoftags, + 'name', '', $this->page->context->id, $this->config->ctx, $this->config->rec); + $this->content->text = $OUTPUT->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($OUTPUT)); + + return $this->content; + } +} diff --git a/tags/classes/privacy/provider.php b/tags/classes/privacy/provider.php new file mode 100644 index 0000000..8ce5159 --- /dev/null +++ b/tags/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_tags. + * + * @package block_tags + * @copyright 2018 Zig Tan <zig@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_tags\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_tags implementing null_provider. + * + * @copyright 2018 Zig Tan <zig@moodle.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/tags/db/access.php b/tags/db/access.php new file mode 100644 index 0000000..7ba3459 --- /dev/null +++ b/tags/db/access.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/>. + +/** + * Tags block caps. + * + * @package block_tags + * @copyright Mark Nelson <markn@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/tags:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/tags: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/tags/edit_form.php b/tags/edit_form.php new file mode 100644 index 0000000..4ea8209 --- /dev/null +++ b/tags/edit_form.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/>. + +/** + * Form for editing tag block instances. + * + * @package block_tags + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Form for editing tag block instances. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_tags_edit_form extends block_edit_form { + protected function specific_definition($mform) { + global $CFG; + // Fields for editing HTML block title and contents. + $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); + + $mform->addElement('text', 'config_title', get_string('configtitle', 'block_tags')); + $mform->setType('config_title', PARAM_TEXT); + $mform->setDefault('config_title', get_string('pluginname', 'block_tags')); + + $this->add_collection_selector($mform); + + $numberoftags = array(); + for ($i = 1; $i <= 200; $i++) { + $numberoftags[$i] = $i; + } + $mform->addElement('select', 'config_numberoftags', get_string('numberoftags', 'blog'), $numberoftags); + $mform->setDefault('config_numberoftags', 80); + + $defaults = array( + core_tag_tag::STANDARD_ONLY => get_string('standardonly', 'block_tags'), + core_tag_tag::BOTH_STANDARD_AND_NOT => get_string('anytype', 'block_tags')); + $mform->addElement('select', 'config_showstandard', get_string('defaultdisplay', 'block_tags'), $defaults); + $mform->setDefault('config_showstandard', core_tag_tag::BOTH_STANDARD_AND_NOT); + + $defaults = array(0 => context_system::instance()->get_context_name()); + $parentcontext = context::instance_by_id($this->block->instance->parentcontextid); + if ($parentcontext->contextlevel > CONTEXT_COURSE) { + $coursecontext = $parentcontext->get_course_context(); + $defaults[$coursecontext->id] = $coursecontext->get_context_name(); + } + if ($parentcontext->contextlevel != CONTEXT_SYSTEM) { + $defaults[$parentcontext->id] = $parentcontext->get_context_name(); + } + $mform->addElement('select', 'config_ctx', get_string('taggeditemscontext', 'block_tags'), $defaults); + $mform->addHelpButton('config_ctx', 'taggeditemscontext', 'block_tags'); + $mform->setDefault('config_ctx', 0); + + $mform->addElement('advcheckbox', 'config_rec', get_string('recursivecontext', 'block_tags')); + $mform->addHelpButton('config_rec', 'recursivecontext', 'block_tags'); + $mform->setDefault('config_rec', 1); + } + + /** + * Add the tag collection selector + * + * @param object $mform the form being built. + */ + protected function add_collection_selector($mform) { + $tagcolls = core_tag_collection::get_collections_menu(false, false, get_string('anycollection', 'block_tags')); + if (count($tagcolls) <= 1) { + return; + } + + $tagcollssearchable = core_tag_collection::get_collections_menu(false, true); + $hasunsearchable = false; + foreach ($tagcolls as $id => $name) { + if ($id && !array_key_exists($id, $tagcollssearchable)) { + $hasunsearchable = true; + $tagcolls[$id] = $name . '*'; + } + } + + $mform->addElement('select', 'config_tagcoll', get_string('tagcollection', 'block_tags'), $tagcolls); + if ($hasunsearchable) { + $mform->addHelpButton('config_tagcoll', 'tagcollection', 'block_tags'); + } + $mform->setDefault('config_tagcoll', 0); + } +} diff --git a/tags/lang/en/block_tags.php b/tags/lang/en/block_tags.php new file mode 100644 index 0000000..afacdd0 --- /dev/null +++ b/tags/lang/en/block_tags.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/>. + +/** + * Strings for component 'block_tags', language 'en', branch 'MOODLE_20_STABLE' + * + * @package block_tags + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['anycollection'] = 'Any'; +$string['anytype'] = 'All'; +$string['configtitle'] = 'Tags block title'; +$string['disabledtags'] = 'Tags are disabled'; +$string['defaultdisplay'] = 'Display tags'; +$string['pluginname'] = 'Tags'; +$string['recursivecontext'] = 'Include child contexts'; +$string['recursivecontext_help'] = 'If unticked, tags of items in the context specified above will be displayed, but not tags of items in lower contexts. For example, course tags may be displayed, but not course activity tags.'; +$string['standardonly'] = 'Only standard'; +$string['tagcollection'] = 'Tag collection'; +$string['tagcollection_help'] = 'Select tag collection to display tags from. If you choose "Any" ' + . 'the tags from all collections except for those marked with * will be displayed'; +$string['taggeditemscontext'] = 'Tagged items context'; +$string['taggeditemscontext_help'] = 'You can limit the tag cloud to the tags that are present in the current course category, course or module'; +$string['tags:addinstance'] = 'Add a new tags block'; +$string['tags:myaddinstance'] = 'Add a new tags block to Dashboard'; +$string['privacy:metadata'] = 'The Tags block only shows data stored in other locations.'; diff --git a/tags/tests/behat/tagcloud.feature b/tags/tests/behat/tagcloud.feature new file mode 100644 index 0000000..6a8bd41 --- /dev/null +++ b/tags/tests/behat/tagcloud.feature @@ -0,0 +1,49 @@ +@block @block_tags @core_tag +Feature: Block tags displaying tag cloud + In order to view system tags + As a user + I need to be able to use the block tags + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | interests | + | teacher1 | Teacher | 1 | teacher1@example.com | Dogs, Cats | + | student1 | Student | 1 | student1@example.com | | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | c1 | + And the following "tags" exist: + | name | isstandard | + | Neverusedtag | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | c1 | editingteacher | + | student1 | c1 | student | + + Scenario: Add Tags block on a front page + When I log in as "admin" + And I am on site homepage + And I follow "Turn editing on" + And I add the "Tags" block + And I log out + And I am on site homepage + Then I should see "Dogs" in the "Tags" "block" + And I should see "Cats" in the "Tags" "block" + And I should not see "Neverusedtag" in the "Tags" "block" + And I click on "Dogs" "link" in the "Tags" "block" + And I should see "You are not logged in" + + Scenario: Add Tags block in a course + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Tags" block + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Dogs" in the "Tags" "block" + And I should see "Cats" in the "Tags" "block" + And I should not see "Neverusedtag" in the "Tags" "block" + And I click on "Dogs" "link" in the "Tags" "block" + And I should see "User interests" in the ".tag-index-items h3" "css_element" + And I should see "Teacher 1" + And I log out diff --git a/tags/version.php b/tags/version.php new file mode 100644 index 0000000..7a24646 --- /dev/null +++ b/tags/version.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/>. + +/** + * Version details + * + * @package block_tags + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2018050800; // Requires this Moodle version +$plugin->component = 'block_tags'; // Full name of the plugin (used for diagnostics) diff --git a/tests/behat/add_blocks.feature b/tests/behat/add_blocks.feature new file mode 100644 index 0000000..4fadd47 --- /dev/null +++ b/tests/behat/add_blocks.feature @@ -0,0 +1,27 @@ +@core @core_block +Feature: Add blocks + In order to add more functionality to pages + As a teacher + I need to add blocks to pages + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + When I add the "Blog menu" block + Then I should see "View my entries about this course" + + @javascript + Scenario: Add a block to a course with Javascript enabled + + Scenario: Add a block to a course with Javascript disabled diff --git a/tests/behat/behat_blocks.php b/tests/behat/behat_blocks.php new file mode 100644 index 0000000..b05d84c --- /dev/null +++ b/tests/behat/behat_blocks.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/>. + +/** + * Steps definitions related with blocks. + * + * @package core_block + * @category test + * @copyright 2012 David Monllaó + * @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. + +use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); + +/** + * Blocks management steps definitions. + * + * @package core_block + * @category test + * @copyright 2012 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_blocks extends behat_base { + + /** + * Adds the selected block. Editing mode must be previously enabled. + * + * @Given /^I add the "(?P<block_name_string>(?:[^"]|\\")*)" block$/ + * @param string $blockname + */ + public function i_add_the_block($blockname) { + $this->execute('behat_forms::i_set_the_field_to', + array("bui_addblock", $this->escape($blockname)) + ); + + // If we are running without javascript we need to submit the form. + if (!$this->running_javascript()) { + $this->execute('behat_general::i_click_on_in_the', + array(get_string('go'), "button", "#add_block", "css_element") + ); + } + } + + /** + * Adds the selected block if it is not already present. Editing mode must be previously enabled. + * + * @Given /^I add the "(?P<block_name_string>(?:[^"]|\\")*)" block if not present$/ + * @param string $blockname + */ + public function i_add_the_block_if_not_present($blockname) { + try { + $this->get_text_selector_node('block', $blockname); + } catch (ElementNotFoundException $e) { + $this->execute('behat_blocks::i_add_the_block', [$blockname]); + } + } + + /** + * Docks a block. Editing mode should be previously enabled. + * + * @Given /^I dock "(?P<block_name_string>(?:[^"]|\\")*)" block$/ + * @param string $blockname + */ + public function i_dock_block($blockname) { + + // Looking for both title and alt. + $xpath = "//input[@type='image'][@title='" . get_string('dockblock', 'block', $blockname) . "' or @alt='" . get_string('addtodock', 'block') . "']"; + $this->execute('behat_general::i_click_on_in_the', + array($xpath, "xpath_element", $this->escape($blockname), "block") + ); + } + + /** + * Opens a block's actions menu if it is not already opened. + * + * @Given /^I open the "(?P<block_name_string>(?:[^"]|\\")*)" blocks action menu$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $blockname + */ + public function i_open_the_blocks_action_menu($blockname) { + + if (!$this->running_javascript()) { + // Action menu does not need to be open if Javascript is off. + return; + } + + // If it is already opened we do nothing. + $blocknode = $this->get_text_selector_node('block', $blockname); + if ($blocknode->hasClass('action-menu-shown')) { + return; + } + + $this->execute('behat_general::i_click_on_in_the', + array(get_string('actions'), "link", $this->escape($blockname), "block") + ); + } + + /** + * Clicks on Configure block for specified block. Page must be in editing mode. + * + * Argument block_name may be either the name of the block or CSS class of the block. + * + * @Given /^I configure the "(?P<block_name_string>(?:[^"]|\\")*)" block$/ + * @param string $blockname + */ + public function i_configure_the_block($blockname) { + // Note that since $blockname may be either block name or CSS class, we can not use the exact label of "Configure" link. + + $this->execute("behat_blocks::i_open_the_blocks_action_menu", $this->escape($blockname)); + + $this->execute('behat_general::i_click_on_in_the', + array("Configure", "link", $this->escape($blockname), "block") + ); + } + + /** + * Ensures that block can be added to the page but does not actually add it. + * + * @Then /^the add block selector should contain "(?P<block_name_string>(?:[^"]|\\")*)" block$/ + * @param string $blockname + */ + public function the_add_block_selector_should_contain_block($blockname) { + $this->execute('behat_forms::the_select_box_should_contain', [get_string('addblock'), $blockname]); + } + + /** + * Ensures that block can not be added to the page. + * + * @Then /^the add block selector should not contain "(?P<block_name_string>(?:[^"]|\\")*)" block$/ + * @param string $blockname + */ + public function the_add_block_selector_should_not_contain_block($blockname) { + $this->execute('behat_forms::the_select_box_should_not_contain', [get_string('addblock'), $blockname]); + } +} diff --git a/tests/behat/configure_block_throughout_site.feature b/tests/behat/configure_block_throughout_site.feature new file mode 100644 index 0000000..84fefa7 --- /dev/null +++ b/tests/behat/configure_block_throughout_site.feature @@ -0,0 +1,73 @@ +@core @core_block +Feature: Add and configure blocks throughout the site + In order to maintain some patterns across all the site + As a manager + I need to set and configure blocks throughout the site + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | teacher1 | teacher | 1 | teacher@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "system role assigns" exist: + | user | course | role | + | manager1 | Acceptance test site | manager | + # Allow at least one role assignment in the block context: + And I log in as "admin" + And I navigate to "Define roles" node in "Site administration > Users > Permissions" + And I follow "Edit Non-editing teacher role" + And I set the following fields to these values: + | Block | 1 | + And I press "Save changes" + And I log out + + Scenario: Add and configure a block throughtout the site + Given I log in as "manager1" + And I am on site homepage + And I follow "Turn editing on" + And I add the "Comments" block + And I configure the "Comments" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + When I am on "Course 1" course homepage + Then I should see "Comments" in the "Comments" "block" + And I should see "Save comment" in the "Comments" "block" + And I am on site homepage + And I configure the "Comments" block + And I set the following fields to these values: + | Default weight | -10 (first) | + And I press "Save changes" + And I am on "Course 1" course homepage + # The first block matching the pattern should be top-left block + And I should see "Comments" in the "//*[@id='region-pre' or @id='block-region-side-pre']/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element" + + Scenario: Blocks on the dashboard page can have roles assigned to them + Given I log in as "manager1" + When I press "Customise this page" + Then I should see "Assign roles in Private files block" + + Scenario: Blocks on courses can have roles assigned to them + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Search forums" block + Then I should see "Assign roles in Search forums block" + + @javascript + Scenario: Blocks can safely be customised + Given I log in as "admin" + And I am on homepage + And I press "Customise this page" + And I add the "HTML" block + And I configure the "(new HTML block)" block + And I set the following fields to these values: + | HTML block title | Foo " onload="document.getElementsByTagName('body')[0].remove()" alt=" | + | Content | Example | + When I press "Save changes" + Then I should see "Course overview" diff --git a/tests/behat/hidden_block_region.feature b/tests/behat/hidden_block_region.feature new file mode 100644 index 0000000..e41f9f2 --- /dev/null +++ b/tests/behat/hidden_block_region.feature @@ -0,0 +1,52 @@ +@core @core_block +Feature: Show hidden blocks in a docked block region when editing + In order to edit blocks in a hidden region + As a teacher + I need to be able to see the blocks when editing is on + + Background: + Given the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | admin | C1 | editingteacher | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I add the "Search forums" block + And I add the "Latest announcements" block + And I add the "Upcoming events" block + And I add the "Recent activity" block + # Hide all the blocks in the non-default region + And I configure the "Search forums" block + And I set the following fields to these values: + | Visible | No | + And I click on "Save changes" "button" + And I configure the "Latest announcements" block + And I set the following fields to these values: + | Visible | No | + And I click on "Save changes" "button" + And I configure the "Upcoming events" block + And I set the following fields to these values: + | Visible | No | + And I click on "Save changes" "button" + And I configure the "Recent activity" block + And I set the following fields to these values: + | Visible | No | + When I click on "Save changes" "button" + # Editing is on so they should be visible + Then I should see "Search forums" + And I should see "Latest announcements" + And I should see "Upcoming events" + And I should see "Recent activity" + And I turn editing mode off + # Editing is off, so they should no longer be visible + And I should not see "Search forums" + And I should not see "Latest announcements" + And I should not see "Upcoming events" + And I should not see "Recent activity" + + @javascript + Scenario: Check that a region with only hidden blocks is not docked in editing mode (javascript enabled) + + Scenario: Check that a region with only hidden blocks is not docked in editing mode (javascript disabled) diff --git a/tests/behat/hide_blocks.feature b/tests/behat/hide_blocks.feature new file mode 100644 index 0000000..db11e3f --- /dev/null +++ b/tests/behat/hide_blocks.feature @@ -0,0 +1,27 @@ +@core @core_block +Feature: Block visibility + In order to configure blocks visibility + As a teacher + I need to show and hide blocks on a page + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + + @javascript + Scenario: Hiding all blocks on the page should remove the column they're in + Given I add the "Search forums" block + And I open the "Search forums" blocks action menu + And I click on "Configure Search forums block" "link" in the "Search forums" "block" + And I set the field "Region" to "Right" + And I press "Save changes" + And I turn editing mode off + And ".empty-region-side-post" "css_element" should not exist in the "body" "css_element" + And I turn editing mode on + And I open the "Search forums" blocks action menu + And I click on "Hide Search forums block" "link" in the "Search forums" "block" + And I follow "Turn editing off" + And ".empty-region-side-post" "css_element" should exist in the "body" "css_element" diff --git a/tests/behat/manage_blocks.feature b/tests/behat/manage_blocks.feature new file mode 100644 index 0000000..19b0fc6 --- /dev/null +++ b/tests/behat/manage_blocks.feature @@ -0,0 +1,60 @@ +@core @core_block +Feature: Block appearances + In order to configure blocks appearance + As a teacher + I need to add and modify block configuration for the page + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I add a "Survey" to section "1" and I fill the form with: + | Name | Test survey name | + | Survey type | ATTLS (20 item version) | + | Description | Test survey description | + And I add a "Book" to section "1" and I fill the form with: + | Name | Test book name | + | Description | Test book description | + And I follow "Test book name" + And I set the following fields to these values: + | Chapter title | Book title | + | Content | Book content test test | + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Comments" block + And I configure the "Comments" block + And I set the following fields to these values: + | Display on page types | Any page | + And I press "Save changes" + + Scenario: Block settings can be modified so that a block apprears on any page + When I follow "Test survey name" + Then I should see "Comments" in the "Comments" "block" + And I am on "Course 1" course homepage + And I configure the "Comments" block + And I set the following fields to these values: + | Display on page types | Any course page | + And I press "Save changes" + And I follow "Turn editing off" + And I follow "Test survey name" + And I should not see "Comments" + + Scenario: Block settings can be modified so that a block can be hidden + When I follow "Test book name" + And I configure the "Comments" block + And I set the following fields to these values: + | Visible | No | + And I press "Save changes" + And I follow "Turn editing off" + And I follow "Test book name" + Then I should not see "Comments" diff --git a/tests/behat/move_blocks.feature b/tests/behat/move_blocks.feature new file mode 100644 index 0000000..6a312ce --- /dev/null +++ b/tests/behat/move_blocks.feature @@ -0,0 +1,46 @@ +@core @core_block +Feature: Block region moving + In order to configure blocks appearance + As a teacher + I need to modify block region for the page + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I add a "Survey" to section "1" and I fill the form with: + | Name | Test survey name | + | Survey type | ATTLS (20 item version) | + | Description | Test survey description | + And I add a "Book" to section "1" and I fill the form with: + | Name | Test book name | + | Description | Test book description | + And I follow "Test book name" + And I set the following fields to these values: + | Chapter title | Book title | + | Content | Book content test test | + And I press "Save changes" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Comments" block + And I configure the "Comments" block + And I set the following fields to these values: + | Display on page types | Any page | + And I press "Save changes" + + Scenario: Block settings can be modified so that a block can be moved + When I follow "Test book name" + And I configure the "Comments" block + And I set the following fields to these values: + | Region | Right | + And I press "Save changes" + And I should see "Comments" in the "//*[@id='region-post' or @id='block-region-side-post']" "xpath_element" diff --git a/tests/behat/restrict_available_blocks.feature b/tests/behat/restrict_available_blocks.feature new file mode 100644 index 0000000..581d0f8 --- /dev/null +++ b/tests/behat/restrict_available_blocks.feature @@ -0,0 +1,38 @@ +@core @core_block +Feature: Allowed blocks controls + In order to prevent the use of some blocks + As an admin + I need to restrict some blocks to be used in courses + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@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 | + + Scenario: Blocks can be added with the default permissions + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I add the "Course completion status" block + And I add the "Activities" block + Then I should see "Activities" in the "Activities" "block" + And I should see "Course completion status" in the "Course completion status" "block" + + Scenario: Blocks can not be added when the admin restricts the permissions + Given I log in as "admin" + And I set the following system permissions of "Teacher" role: + | block/activity_modules:addinstance | Prohibit | + And I am on "Course 1" course homepage + And I navigate to "Users > Permissions" in current page administration + And I override the system permissions of "Teacher" role with: + | block/completionstatus:addinstance | Prohibit | + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + Then the add block selector should not contain "Activities" block + And the add block selector should not contain "Course completion status" block diff --git a/tests/behat/return_block_original_state.feature b/tests/behat/return_block_original_state.feature new file mode 100644 index 0000000..79c4b97 --- /dev/null +++ b/tests/behat/return_block_original_state.feature @@ -0,0 +1,47 @@ +@core @core_block +Feature: The context of a block can always be returned to it's original state. + In order to revert actions when configuring blocks + As an admin + I need to be able to return the block to original state + + Scenario: Add and configure a block to display on every page and revert back + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And I log in as "admin" + When I am on "Course 1" course homepage with editing mode on + And I add the "Tags" block + Then I should see "Tags" in the "Tags" "block" + And I navigate to course participants + And I configure the "Tags" block + And I set the following fields to these values: + | Display on page types | Any page | + And I press "Save changes" + And I am on "Course 1" course homepage + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Assignment1 | + | Description | Description | + And I follow "Assignment1" + And I configure the "Tags" block + And I set the following fields to these values: + | Display on page types | Any assignment module page | + And I press "Save changes" + And I should see "Tags" in the "Tags" "block" + And I am on "Course 1" course homepage + And "Tags" "block" should not exist + And I navigate to course participants + And "Tags" "block" should not exist + And I am on "Course 1" course homepage + And I add a "Assignment" to section "1" and I fill the form with: + | Assignment name | Assignment2 | + | Description | Description | + And I follow "Assignment2" + And I should see "Tags" in the "Tags" "block" + And I configure the "Tags" block + And I set the following fields to these values: + | Display on page types | Any page | + And I press "Save changes" + And I am on "Course 1" course homepage + And I should see "Tags" in the "Tags" "block" + And I navigate to course participants + And I should see "Tags" in the "Tags" "block" diff --git a/tests/externallib_test.php b/tests/externallib_test.php new file mode 100644 index 0000000..fdf3bea --- /dev/null +++ b/tests/externallib_test.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/>. + +/** + * External block functions unit tests + * + * @package core_block + * @category external + * @copyright 2017 Juan Leyva <juan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.3 + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * External block functions unit tests + * + * @package core_block + * @category external + * @copyright 2015 Juan Leyva <juan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.0 + */ +class core_block_externallib_testcase extends externallib_advanced_testcase { + + /** + * Test get_course_blocks + */ + public function test_get_course_blocks() { + global $DB, $FULLME; + + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); + + $page = new moodle_page(); + $page->set_context(context_course::instance($course->id)); + $page->set_pagelayout('course'); + $course->format = course_get_format($course)->get_format(); + $page->set_pagetype('course-view-' . $course->format); + $page->blocks->load_blocks(); + $newblock = 'calendar_upcoming'; + $page->blocks->add_block_at_end_of_default_region($newblock); + $this->setUser($user); + + // Check for the new block. + $result = core_block_external::get_course_blocks($course->id); + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result); + + // Expect the new block. + $this->assertCount(1, $result['blocks']); + $this->assertEquals($newblock, $result['blocks'][0]['name']); + } + + /** + * Test get_course_blocks on site home + */ + public function test_get_course_blocks_site_home() { + global $DB, $FULLME; + + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + + $page = new moodle_page(); + $page->set_context(context_course::instance(SITEID)); + $page->set_pagelayout('frontpage'); + $page->set_pagetype('site-index'); + $page->blocks->load_blocks(); + $newblock = 'calendar_upcoming'; + $page->blocks->add_block_at_end_of_default_region($newblock); + $this->setUser($user); + + // Check for the new block. + $result = core_block_external::get_course_blocks(SITEID); + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result); + + // Expect the new block. + $this->assertCount(1, $result['blocks']); + $this->assertEquals($newblock, $result['blocks'][0]['name']); + } + + /** + * Test get_course_blocks + */ + public function test_get_course_blocks_overrides() { + global $DB, $CFG, $FULLME; + + $this->resetAfterTest(true); + + $CFG->defaultblocks_override = 'participants,search_forums,course_list:calendar_upcoming,recent_activity'; + + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); + + $this->setUser($user); + + // Try default blocks. + $result = core_block_external::get_course_blocks($course->id); + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result); + + // Expect 5 default blocks. + $this->assertCount(5, $result['blocks']); + + $expectedblocks = array('navigation', 'settings', 'participants', 'search_forums', 'course_list', + 'calendar_upcoming', 'recent_activity'); + foreach ($result['blocks'] as $block) { + if (!in_array($block['name'], $expectedblocks)) { + $this->fail("Unexpected block found: " . $block['name']); + } + } + + } + +} diff --git a/tests/privacy_test.php b/tests/privacy_test.php new file mode 100644 index 0000000..8c6327e --- /dev/null +++ b/tests/privacy_test.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/>. + +/** + * Data provider tests. + * + * @package core_block + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart <fred@branchup.tech> + * @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 core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use core_block\privacy\provider; + +/** + * Data provider testcase class. + * + * @package core_block + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart <fred@branchup.tech> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_block_privacy_testcase extends provider_testcase { + + public function setUp() { + $this->resetAfterTest(); + } + + public function test_get_contexts_for_userid() { + $dg = $this->getDataGenerator(); + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $manager = $this->get_block_manager(['region-a'], $c1ctx); + $manager->add_block('myprofile', 'region-a', 0, false); + $manager->load_blocks(); + $blockmyprofile = $manager->get_blocks_for_region('region-a')[0]; + + $manager = $this->get_block_manager(['region-a'], $c2ctx); + $manager->add_block('login', 'region-a', 0, false); + $manager->add_block('mentees', 'region-a', 1, false); + $manager->load_blocks(); + list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a'); + + $manager = $this->get_block_manager(['region-a'], $u1ctx); + $manager->add_block('private_files', 'region-a', 0, false); + $manager->load_blocks(); + $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0]; + + $this->set_hidden_pref($blocklogin, true, $u1->id); + $this->set_hidden_pref($blockprivatefiles, true, $u1->id); + $this->set_docked_pref($blockmyprofile, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u2->id); + + $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(4, $contextids); + $this->assertTrue(in_array($blocklogin->context->id, $contextids)); + $this->assertTrue(in_array($blockprivatefiles->context->id, $contextids)); + $this->assertTrue(in_array($blockmyprofile->context->id, $contextids)); + $this->assertTrue(in_array($blockmentees->context->id, $contextids)); + + $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(1, $contextids); + $this->assertTrue(in_array($blockmentees->context->id, $contextids)); + } + + public function test_delete_data_for_user() { + global $DB; + $dg = $this->getDataGenerator(); + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $manager = $this->get_block_manager(['region-a'], $c1ctx); + $manager->add_block('myprofile', 'region-a', 0, false); + $manager->load_blocks(); + $blockmyprofile = $manager->get_blocks_for_region('region-a')[0]; + + $manager = $this->get_block_manager(['region-a'], $c2ctx); + $manager->add_block('login', 'region-a', 0, false); + $manager->add_block('mentees', 'region-a', 1, false); + $manager->load_blocks(); + list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a'); + + $manager = $this->get_block_manager(['region-a'], $u1ctx); + $manager->add_block('private_files', 'region-a', 0, false); + $manager->load_blocks(); + $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0]; + + $this->set_hidden_pref($blocklogin, true, $u1->id); + $this->set_hidden_pref($blocklogin, true, $u2->id); + $this->set_hidden_pref($blockprivatefiles, true, $u1->id); + $this->set_hidden_pref($blockmyprofile, true, $u1->id); + $this->set_docked_pref($blockmyprofile, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u2->id); + + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + + provider::delete_data_for_user(new approved_contextlist($u1, 'core_block', [$blocklogin->context->id, + $blockmyprofile->context->id, $blockmentees->context->id])); + + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + } + + public function test_delete_data_for_all_users_in_context() { + global $DB; + $dg = $this->getDataGenerator(); + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $manager = $this->get_block_manager(['region-a'], $c1ctx); + $manager->add_block('myprofile', 'region-a', 0, false); + $manager->load_blocks(); + $blockmyprofile = $manager->get_blocks_for_region('region-a')[0]; + + $manager = $this->get_block_manager(['region-a'], $c2ctx); + $manager->add_block('login', 'region-a', 0, false); + $manager->add_block('mentees', 'region-a', 1, false); + $manager->load_blocks(); + list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a'); + + $manager = $this->get_block_manager(['region-a'], $u1ctx); + $manager->add_block('private_files', 'region-a', 0, false); + $manager->load_blocks(); + $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0]; + + $this->set_hidden_pref($blocklogin, true, $u1->id); + $this->set_hidden_pref($blocklogin, true, $u2->id); + $this->set_hidden_pref($blockprivatefiles, true, $u1->id); + $this->set_hidden_pref($blockmyprofile, true, $u1->id); + $this->set_docked_pref($blockmyprofile, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u1->id); + $this->set_docked_pref($blockmentees, true, $u2->id); + + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + + // Nothing happens. + provider::delete_data_for_all_users_in_context($c1ctx); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + + // Delete one block. + provider::delete_data_for_all_users_in_context($blocklogin->context); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + + // Delete another block. + provider::delete_data_for_all_users_in_context($blockmyprofile->context); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "block{$blocklogin->instance->id}hidden"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockprivatefiles->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "block{$blockmyprofile->instance->id}hidden"])); + $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmyprofile->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id, + 'name' => "docked_block_instance_{$blockmentees->instance->id}"])); + } + + public function test_export_data_for_user() { + global $DB; + $dg = $this->getDataGenerator(); + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + $yes = transform::yesno(true); + $no = transform::yesno(false); + + $manager = $this->get_block_manager(['region-a'], $c1ctx); + $manager->add_block('myprofile', 'region-a', 0, false); + $manager->add_block('login', 'region-a', 1, false); + $manager->add_block('mentees', 'region-a', 2, false); + $manager->add_block('private_files', 'region-a', 3, false); + $manager->load_blocks(); + list($bmyprofile, $blogin, $bmentees, $bprivatefiles) = $manager->get_blocks_for_region('region-a'); + + // Set some user preferences. + $this->set_hidden_pref($blogin, true, $u1->id); + $this->set_docked_pref($blogin, false, $u1->id); + $this->set_docked_pref($blogin, true, $u2->id); + $this->set_hidden_pref($bprivatefiles, false, $u1->id); + $this->set_docked_pref($bprivatefiles, true, $u2->id); + $this->set_docked_pref($bmyprofile, true, $u1->id); + $this->set_docked_pref($bmentees, true, $u2->id); + + // Export data. + provider::export_user_data(new approved_contextlist($u1, 'core_block', [$bmyprofile->context->id, $blogin->context->id, + $bmentees->context->id, $bprivatefiles->context->id])); + $prefs = writer::with_context($bmentees->context)->get_user_context_preferences('core_block'); + $this->assertEmpty((array) $prefs); + + $prefs = writer::with_context($blogin->context)->get_user_context_preferences('core_block'); + $this->assertEquals($no, $prefs->block_is_docked->value); + $this->assertEquals($yes, $prefs->block_is_hidden->value); + + $prefs = writer::with_context($bprivatefiles->context)->get_user_context_preferences('core_block'); + $this->assertObjectNotHasAttribute('block_is_docked', $prefs); + $this->assertEquals($no, $prefs->block_is_hidden->value); + + $prefs = writer::with_context($bmyprofile->context)->get_user_context_preferences('core_block'); + $this->assertEquals($yes, $prefs->block_is_docked->value); + $this->assertObjectNotHasAttribute('block_is_hidden', $prefs); + } + + /** + * Get the block manager. + * + * @param array $regions The regions. + * @param context $context The context. + * @param string $pagetype The page type. + * @param string $subpage The sub page. + * @return block_manager + */ + protected function get_block_manager($regions, $context, $pagetype = 'page-type', $subpage = '') { + $page = new moodle_page(); + $page->set_context($context); + $page->set_pagetype($pagetype); + $page->set_subpage($subpage); + $page->set_url(new moodle_url('/')); + + $blockmanager = new block_manager($page); + $blockmanager->add_regions($regions, false); + $blockmanager->set_default_region($regions[0]); + + return $blockmanager; + } + + /** + * Set a docked preference. + * + * @param block_base $block The block. + * @param bool $value The value. + * @param int $userid The user ID. + */ + protected function set_docked_pref($block, $value, $userid) { + set_user_preference("docked_block_instance_{$block->instance->id}", $value, $userid); + } + + /** + * Set a hidden preference. + * + * @param block_base $block The block. + * @param bool $value The value. + * @param int $userid The user ID. + */ + protected function set_hidden_pref($block, $value, $userid) { + set_user_preference("block{$block->instance->id}hidden", $value, $userid); + } + +} diff --git a/upgrade 14.04.12.txt b/upgrade 14.04.12.txt new file mode 100644 index 0000000..7ef8f0a --- /dev/null +++ b/upgrade 14.04.12.txt @@ -0,0 +1,82 @@ +This files describes API changes in /blocks/* - activity modules, +information provided here is intended especially for developers. + +=== 3.4 === + +* The block_instances table now contains fields timecreated and timemodified. If third-party code + creates or updates these rows (without using the standard API), it should be modified to set + these fields as appropriate. +* Blocks can now be included in Moodle global search, with some limitations (at present, the search + works only for blocks located directly on course pages or site home page). See the HTML block for + an example. +* Block block_messages is no longer a part of core. + +=== 3.3 === + +* block_manager::get_required_by_theme_block_types() is no longer static. +* The plugin block_course_overview has been removed from core and is being replaced by block_myoverview. + During the upgrade process the block_course_overview block will be uninstalled and all its settings will be deleted. + If you wish to keep the block_course_overview block and its settings, download it from moodle.org and put it back in + the blocks/ directory BEFORE UPGRADING. + +=== 3.1 === + +* The collapsed class was removed from the navigation block to make it compatible with aria. +* New aria attributes were added on the navigation block [aria-expanded="false"]. +* The tree JS handling were moved from YUI to AMD module (Jquery). + +=== 2.9 === + +* The obsolete method preferred_width() was removed (it was not doing anything) +* Deprecated block_base::config_save as is not called anywhere and should not be used. +* Added instance_copy() function to the block_base class. This function allows for block + specific data to be copied when a block is copied. + +=== 2.8 === + +* The instance_config_print() function was removed. It was deprecated in + Moodle 2.0, but without debugging notices. Since it was no longer a part + of the code path, debugging notices would not have been displayed. +* Deprecated functions were removed from the block_base class: +** _print_block() +** _print_shadow() +** _title_html() +** _add_edit_controls() +** config_print() + +=== 2.6 === + +* Deprecated /admin/block.php was removed, make sure blocks are using settings.php instead. + +=== 2.4 === + +Created new capability 'blocks/xxx:myaddinstance' that determines whether a user can add +a specific block to their My Home page. This capability was only defined for blocks where +the applicable_formats function does not include "'my' => false" in the returned array, +allowing it be added to the My Home page. + +=== 2.3 === + +required changes in code: +* block_xxx_pluginfile() is now given the 7th parameter (hopefully the last one) that + contains additional options for the file serving. The array should be re-passed + to send_stored_file(). + +=== 2.0 === + +required changes in code: +* use new DML syntax everywhere +* use new DDL syntax in db/upgrade.php +* replace defaults.php by settings.php and db/install.php +* replace STATEMENTS section in db/install.xml by db/install.php +* move post instalation code from install() method into db/install.php +* completely rewrite file handling +* rewrite backup/restore +* theme changes: move plugin styles into blocks/xxx/styles.css and use new css markers for images, + move all images into new blocks/xxx/pix/ directory and use new outputlib api + old global $THEME is fully replaced by $OUTPUT +* remove '_utf8' from language pack names, use new {$a} syntax in language packs +* use 'pluginname' lang pack identifier instead of 'blockname' +* move cron and version number into standard version.php +* removed support for old config_global.html, use settings.php + -- GitLab