From 56f55b9ac2d62a34f1d5fc787db8036c0f05a82b Mon Sep 17 00:00:00 2001 From: prasanna-lmsace Date: Mon, 25 Sep 2023 05:20:46 +0530 Subject: [PATCH] Automation initial source ci test --- .gitignore | 100 ++ README.md | 631 +++++++++- .../amd/build/chaptersource.min.js | 12 + .../amd/build/chaptersource.min.js.map | 1 + .../amd/build/notification.min.js | 3 + .../amd/build/notification.min.js.map | 1 + actions/notification/amd/src/chaptersource.js | 188 +++ actions/notification/classes/actionform.php | 720 +++++++++++ actions/notification/classes/external.php | 87 ++ .../classes/local/entities/notification.php | 306 +++++ actions/notification/classes/notification.php | 988 +++++++++++++++ .../reportbuilder/datasource/notification.php | 136 ++ actions/notification/classes/schedule.php | 540 ++++++++ .../classes/task/notify_users.php | 178 +++ actions/notification/db/access.php | 47 + actions/notification/db/events.php | 31 + actions/notification/db/install.xml | 91 ++ actions/notification/db/services.php | 37 + actions/notification/db/tasks.php | 37 + .../lang/en/pulseaction_notification.php | 139 +++ actions/notification/lib.php | 185 +++ .../notification/templates/preview.mustache | 17 + .../behat/behat_pulseaction_notification.php | 106 ++ .../behat/pulse_notification_template.feature | 136 ++ actions/notification/version.php | 28 + amd/build/automation.min.js | 3 + amd/build/automation.min.js.map | 1 + amd/build/completion.min.js | 14 +- amd/build/completion.min.js.map | 2 +- amd/build/events.min.js | 11 + amd/build/events.min.js.map | 1 + amd/build/modal_preset.min.js | 3 + amd/build/modal_preset.min.js.map | 1 + amd/build/module.min.js | 14 +- amd/build/module.min.js.map | 2 +- amd/build/preset.min.js | 3 + amd/build/preset.min.js.map | 1 + amd/src/automation.js | 192 +++ amd/src/completion.js | 2 +- amd/src/events.js | 30 + amd/src/modal_preset.js | 99 ++ amd/src/module.js | 143 ++- amd/src/preset.js | 247 ++++ approve.php | 2 +- assets/preset-congratulations.mbz | Bin 0 -> 5321 bytes assets/preset-disclaimer.mbz | Bin 0 -> 5153 bytes assets/preset-inbox_micro_learning.mbz | Bin 0 -> 5816 bytes assets/preset-teacher_approval.mbz | Bin 0 -> 5163 bytes assets/preset-welcome_message.mbz | Bin 0 -> 5291 bytes assets/presets.xml | 33 + automation/automationlib.php | 94 ++ automation/instances/edit.php | 154 +++ automation/instances/list.php | 197 +++ automation/templates/edit.php | 106 ++ automation/templates/list.php | 205 +++ backup/moodle2/backup_pulse_stepslib.php | 9 +- .../restore_pulse_activity_task.class.php | 2 +- backup/moodle2/restore_pulse_stepslib.php | 20 +- classes/automation/action_base.php | 362 ++++++ classes/automation/condition_base.php | 226 ++++ classes/automation/helper.php | 377 ++++++ classes/automation/instances.php | 655 ++++++++++ classes/automation/templates.php | 475 +++++++ classes/completion/custom_completion.php | 24 +- classes/eventobserver.php | 44 +- classes/extendpro.php | 283 +++++ classes/external.php | 88 ++ classes/forms/automation_instance_form.php | 233 ++++ classes/forms/automation_template_form.php | 327 +++++ classes/helper.php | 584 +++++++++ classes/plugininfo/pulseaction.php | 157 +++ classes/plugininfo/pulsecondition.php | 155 +++ classes/preset.php | 850 +++++++++++++ classes/privacy/provider.php | 10 +- .../pulse_course_modinfo.php | 11 +- classes/table/approveuser.php | 18 +- classes/table/approveuser_search.php | 18 +- classes/table/auto_instances.php | 270 ++++ classes/table/auto_templates.php | 242 ++++ classes/table/automation_filterset.php | 66 + classes/task/notify_users.php | 131 +- classes/task/sendinvitation.php | 100 +- classes/task/update_completion.php | 145 ++- conditions/activity/classes/conditionform.php | 172 +++ conditions/activity/db/events.php | 32 + .../lang/en/pulsecondition_activity.php | 35 + .../trigger_pulsecondition_activity.feature | 60 + conditions/activity/version.php | 27 + conditions/cohort/classes/conditionform.php | 137 ++ conditions/cohort/db/events.php | 36 + .../cohort/lang/en/pulsecondition_cohort.php | 33 + .../trigger_pulsecondition_cohort.feature | 66 + conditions/cohort/version.php | 27 + conditions/course/classes/conditionform.php | 107 ++ conditions/course/db/events.php | 32 + .../course/lang/en/pulsecondition_course.php | 28 + .../trigger_pulsecondition_course.feature | 52 + conditions/course/version.php | 28 + .../enrolment/classes/conditionform.php | 99 ++ conditions/enrolment/db/events.php | 40 + .../lang/en/pulsecondition_enrolment.php | 28 + .../trigger_pulsecondition_enrolment.feature | 52 + conditions/enrolment/version.php | 27 + conditions/session/classes/conditionform.php | 212 ++++ conditions/session/db/events.php | 37 + .../lang/en/pulsecondition_session.php | 30 + .../trigger_pulsecondition_session.feature | 69 + conditions/session/version.php | 28 + db/access.php | 38 + db/events.php | 11 +- db/install.php | 41 + db/install.xml | 94 +- db/services.php | 37 + db/subplugins.json | 6 + db/upgrade.php | 176 ++- lang/en/pulse.php | 280 ++++- lib.php | 1107 ++++------------- lib/vars.php | 283 ++++- mod_form.php | 86 +- pix/monologo.svg | 1 + preset_restore.php | 89 ++ settings.php | 46 + styles.css | 302 ++++- templates/automation_tabs.mustache | 9 + templates/modal_preset.mustache | 38 + templates/overrides.mustache | 14 + templates/preset.mustache | 53 + templates/presets_list.mustache | 95 ++ templates/status_switch.mustache | 58 + tests/behat/behat_pulse.php | 88 ++ tests/behat/pulse_activity_completion.feature | 2 +- tests/behat/pulse_appearance.feature | 67 + tests/behat/pulse_automation_instance.feature | 173 +++ tests/behat/pulse_automation_template.feature | 282 +++++ tests/behat/pulse_preset.feature | 67 + tests/lib_test.php | 66 +- tests/preset_test.php | 138 ++ version.php | 10 +- view.php | 5 +- 139 files changed, 16337 insertions(+), 1106 deletions(-) create mode 100644 .gitignore create mode 100644 actions/notification/amd/build/chaptersource.min.js create mode 100644 actions/notification/amd/build/chaptersource.min.js.map create mode 100644 actions/notification/amd/build/notification.min.js create mode 100644 actions/notification/amd/build/notification.min.js.map create mode 100644 actions/notification/amd/src/chaptersource.js create mode 100644 actions/notification/classes/actionform.php create mode 100644 actions/notification/classes/external.php create mode 100644 actions/notification/classes/local/entities/notification.php create mode 100644 actions/notification/classes/notification.php create mode 100644 actions/notification/classes/reportbuilder/datasource/notification.php create mode 100644 actions/notification/classes/schedule.php create mode 100644 actions/notification/classes/task/notify_users.php create mode 100644 actions/notification/db/access.php create mode 100644 actions/notification/db/events.php create mode 100644 actions/notification/db/install.xml create mode 100644 actions/notification/db/services.php create mode 100644 actions/notification/db/tasks.php create mode 100644 actions/notification/lang/en/pulseaction_notification.php create mode 100644 actions/notification/lib.php create mode 100644 actions/notification/templates/preview.mustache create mode 100644 actions/notification/tests/behat/behat_pulseaction_notification.php create mode 100644 actions/notification/tests/behat/pulse_notification_template.feature create mode 100644 actions/notification/version.php create mode 100644 amd/build/automation.min.js create mode 100644 amd/build/automation.min.js.map create mode 100644 amd/build/events.min.js create mode 100644 amd/build/events.min.js.map create mode 100644 amd/build/modal_preset.min.js create mode 100644 amd/build/modal_preset.min.js.map create mode 100644 amd/build/preset.min.js create mode 100644 amd/build/preset.min.js.map create mode 100644 amd/src/automation.js create mode 100644 amd/src/events.js create mode 100644 amd/src/modal_preset.js create mode 100644 amd/src/preset.js create mode 100644 assets/preset-congratulations.mbz create mode 100644 assets/preset-disclaimer.mbz create mode 100644 assets/preset-inbox_micro_learning.mbz create mode 100644 assets/preset-teacher_approval.mbz create mode 100644 assets/preset-welcome_message.mbz create mode 100644 assets/presets.xml create mode 100644 automation/automationlib.php create mode 100644 automation/instances/edit.php create mode 100644 automation/instances/list.php create mode 100644 automation/templates/edit.php create mode 100644 automation/templates/list.php create mode 100644 classes/automation/action_base.php create mode 100644 classes/automation/condition_base.php create mode 100644 classes/automation/helper.php create mode 100644 classes/automation/instances.php create mode 100644 classes/automation/templates.php create mode 100644 classes/extendpro.php create mode 100644 classes/external.php create mode 100644 classes/forms/automation_instance_form.php create mode 100644 classes/forms/automation_template_form.php create mode 100644 classes/helper.php create mode 100644 classes/plugininfo/pulseaction.php create mode 100644 classes/plugininfo/pulsecondition.php create mode 100644 classes/preset.php rename locallib.php => classes/pulse_course_modinfo.php (91%) create mode 100644 classes/table/auto_instances.php create mode 100644 classes/table/auto_templates.php create mode 100644 classes/table/automation_filterset.php create mode 100644 conditions/activity/classes/conditionform.php create mode 100644 conditions/activity/db/events.php create mode 100644 conditions/activity/lang/en/pulsecondition_activity.php create mode 100644 conditions/activity/tests/behat/trigger_pulsecondition_activity.feature create mode 100644 conditions/activity/version.php create mode 100644 conditions/cohort/classes/conditionform.php create mode 100644 conditions/cohort/db/events.php create mode 100644 conditions/cohort/lang/en/pulsecondition_cohort.php create mode 100644 conditions/cohort/tests/behat/trigger_pulsecondition_cohort.feature create mode 100644 conditions/cohort/version.php create mode 100644 conditions/course/classes/conditionform.php create mode 100644 conditions/course/db/events.php create mode 100644 conditions/course/lang/en/pulsecondition_course.php create mode 100644 conditions/course/tests/behat/trigger_pulsecondition_course.feature create mode 100644 conditions/course/version.php create mode 100644 conditions/enrolment/classes/conditionform.php create mode 100644 conditions/enrolment/db/events.php create mode 100644 conditions/enrolment/lang/en/pulsecondition_enrolment.php create mode 100644 conditions/enrolment/tests/behat/trigger_pulsecondition_enrolment.feature create mode 100644 conditions/enrolment/version.php create mode 100644 conditions/session/classes/conditionform.php create mode 100644 conditions/session/db/events.php create mode 100644 conditions/session/lang/en/pulsecondition_session.php create mode 100644 conditions/session/tests/behat/trigger_pulsecondition_session.feature create mode 100644 conditions/session/version.php create mode 100644 db/install.php create mode 100644 db/services.php create mode 100644 db/subplugins.json create mode 100644 pix/monologo.svg create mode 100644 preset_restore.php create mode 100644 settings.php create mode 100644 templates/automation_tabs.mustache create mode 100644 templates/modal_preset.mustache create mode 100644 templates/overrides.mustache create mode 100644 templates/preset.mustache create mode 100644 templates/presets_list.mustache create mode 100644 templates/status_switch.mustache create mode 100644 tests/behat/pulse_appearance.feature create mode 100644 tests/behat/pulse_automation_instance.feature create mode 100644 tests/behat/pulse_automation_template.feature create mode 100644 tests/behat/pulse_preset.feature create mode 100644 tests/preset_test.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76a7e9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# These are some examples of commonly ignored file patterns. + +# You should customize this list as applicable to your project. + +# Learn more about .gitignore: + +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + + + +# Node artifact files + +node_modules/ + +dist/ + + + +# Compiled Java class files + +*.class + + + +# Compiled Python bytecode + +*.py[cod] + + + +# Log files + +*.log + + + +# Package files + +*.jar + + + +# Maven + +target/ + +dist/ + + + +# JetBrains IDE + +.idea/ + + + +# Unit test reports + +TEST*.xml + + + +# Generated by MacOS + +.DS_Store + + + +# Generated by Windows + +Thumbs.db + + + +# Applications + +*.app + +*.exe + +*.war + + + +# Large media files + +*.mp4 + +*.tiff + +*.avi + +*.flv + +*.mov + +*.wmv + + + diff --git a/README.md b/README.md index 73b7238..bb3eb91 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,607 @@ -# Pulse -Moodle activity plugin to improve student engagement and compliance. +# About Pulse - 2.0 -# Requirements -This plugin requires Moodle 3.9+ +## Background -# Motivation for this plugin -This plugin was built to enable teachers to build sophisticated learning journeys for a various use cases, for example: -- Drip feed content -- Re-engage students -- Gather feedback +Pulse revolves around the idea of automating courses. In its current form, this is achieved through an activity that triggers an action based on availability status. Moodle provides various availability conditions that can be deeply integrated with learning content, and additional triggers can be added by creating availability conditions. -# Installation -Install the plugin like any other plugin to folder /mod/pulse -See http://docs.moodle.org/en/Installing_plugins for details on installing Moodle plugins +While this approach is highly flexible, it may not be immediately intuitive as it combines course content with automation. While both are integral to a course's learning path, not all teachers may find this approach comfortable. Another challenge is that determining availability on a large scale can be resource-intensive and require a robust server. -# Initial Configuration -After installing the plugin, it is ready to use without the need for any configuration. +To address these issues, we aim to revamp the architecture of Pulse for the next version. Our goals include improving scalability, enhancing robustness, and simplifying usability. We discuss our general goals, followed by requirements and implementation notes. We then introduce the concept of automation templates, which can be used to create automation instances in courses. -# Theme support -This plugin is developed and tested on Moodle Core's Boost theme. It should also work with Boost child themes, including Moodle Core's Classic theme. However, we can't support any other theme than Boost. +Finally, we specify the requirements for a report source for Moodle's custom report builder, which will be used for a notification queue. -# Plugin repositories -This plugin will be published and regularly updated in the Moodle plugins repository: https://moodle.org/plugins/mod_pulse -The latest development version can be found on Github: https://github.com/bdecentgmbh/moodle-mod_pulse +## Goals -# Bug and problem reports / Support requests -This plugin is carefully developed and thoroughly tested, but bugs and problems can always appear. -Please report bugs and problems on Github: https://github.com/bdecentgmbh/moodle-mod_pulse/issues -We will do our best to solve your problems, but please note that due to limited resources we can't always provide per-case support. +Based on our own experience with Pulse in various projects, as well as input from customers and partners, we aim to achieve the following goals with Pulse 2.0: -# Feature proposals -Please issue feature proposals on Github: https://github.com/bdecentgmbh/moodle-mod_pulse/issues -Please create pull requests on Github: https://github.com/bdecentgmbh/moodle-mod_pulse/pulls -We are always interested to read about your feature proposals or even get a pull request from you, but please accept that we can handle your issues only as feature proposals and not as feature requests. +**Focused:** We developed Pulse because we consistently encountered specific issues within courses. These issues, while somewhat heterogeneous due to their course-based nature, have made it challenging to immediately grasp the essence of Pulse. Therefore, the next release will refine Pulse into a product primarily designed for course automation, with the initial major focus on notifications. -# Moodle release support -This plugin is maintained for the two most recent major releases of Moodle as well as the most recent LTS release of Moodle. -If you are running a legacy version of Moodle, but want or need to run the latest version of this plugin, you can get the latest version of the plugin, remove the line starting with $plugin->requires from version.php and use this latest plugin version then on your legacy Moodle. However, please note that you will run this setup completely at your own risk. We can't support this approach in any way and there is an undeniable risk for erratic behavior. +**Robust:** Despite its high flexibility, Pulse has occasionally proven to be somewhat finicky, particularly concerning the availability status. This fragility is attributable to the diverse array of availability conditions and their susceptibility to changes. In the next release, we intend to enhance Pulse's robustness and make troubleshooting easier. + +**Scalable:** Pulse's current resource-intensive demands stem from the complex queries required to determine availability status. Our goal for the next release is to ensure that Pulse functions smoothly on standard Moodle hosting infrastructure, even for mid-sized installations, without resource-related issues. + +**Intuitive:** While Pulse's integration as a course activity is advantageous for the learning path, customers have expressed reservations about including an activity in the course that, in many cases, should remain hidden from students. Therefore, the next release of Pulse will empower learning designers and teachers to choose whether or not to display an activity. + +**Easier to maintain:** Pulse comes with presets to simplify its use for teachers. However, once a preset has been applied, any subsequent changes to the template do not affect existing Pulse activities, posing significant maintenance challenges, especially on larger sites with potentially hundreds of courses, each containing numerous Pulse activities. In the next release, site administrators will gain the capability to create, update, and globally deploy automations, with the option to override them on a course/activity level. + +## Architecture + +The new Pulse architecture comprises the following key components: + +1. **Automation Templates:** + + These are globally managed by users with the appropriate capabilities. + +2. **Automation Instances:** + + These instances are created based on automation templates and are kept in sync with the automation template. They offer the option to override specific settings per instance. + +3. **Automation Conditions:** + + Conditions trigger the automation and rely on events and/or completion. They are built in a modular way. + +4. **Automation Actions:** + + These represent the outcomes of the automation, determining what actually happens. They are built in a modular way, with the initial scope primarily focusing on notifications. + +# Pulse - General settings + +**Detailed log** + +Display a detailed log for a scheduled task, but only use it for troubleshooting purposes and disable it on a production site. + +**Number of schedule count** + +This setting allows you to control how many scheduled task notifications are sent during each cron run. By specifying a numerical value, you can regulate the rate at which system administrators receive notifications regarding the completion or status of scheduled tasks. + + +![Pulse-general-setting](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/fe81d840-4fb0-4c7f-a605-c09d8e7a3853) + + +# Automation templates + +Users with the appropriate permissions create automation templates globally, outside of courses. The template itself doesn't perform any actions; it serves as the foundation for instances. + +## Relation between templates and instances + +The relationship between templates and instances ensures that settings defined in the template are synchronized with instances based on that template, except when a specific setting in an instance has been overridden. This means that any changes made to a setting in the template will automatically apply to all instances derived from the template. + +For each setting within an instance, there is an override toggle available to protect locally made changes. Settings that have been locally modified will not be affected by changes to the same setting in the template. + +Within the template, there is information indicating the number of instances where a setting has been locally overridden. Clicking on this number will open a modal with a link to the corresponding automation instance. + +# Manage Automation templates lists + +Automation templates can function in different ways based on their configuration. They can serve as 'default' templates that are applied to new courses, they can be mandated for every course, or they can be used to create an automation instance from within a course. + +![Pulse-automation-template-lists](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/ab734e1e-759f-4d11-928b-8c285eb44f67) + + +***Create New Template*** + +The 'Create new template' button that allow you to create custom templates for Automation templates. + +***Sort*** + +It provides users with the ability to arrange and display a list of automation templates in a Alphabetic order by the 'Preferences'. + +***Filter*** + +It enables to filter and display a list of templates based on predefined categories. + +***Circle with Icon*** + +The icon represents the enabled actions in the automation template. The following actions are available: 'Notification,' 'Assignment,' 'Membership,' and 'Skills'. + +***Template Title Name*** + +The title of the automation template should provide a generic explanation of its purpose. + +***Pencil icon*** + +You can edit the template title by clicking on the pencil icon next to it. + +***Notification Pills*** + +The pills provide additional important information about the automation template. In this case, it explains that it's a notification + +***Preferences*** + +This serves as the reference for the template, providing a unique identifier. It will be part of the unique identifier for the automation instance. + +***Cog icon*** + +Click on this icon to edit the template. + +***Eye icon*** + +Click on this icon to toggle the visibility of a template. A template that is not visible will be hidden in courses. Existing automation instances will still be available, but new ones cannot be added anymore. + +***Toggle Button*** + +Use this toggle to enable or disable a template. When a template is disabled, it also disables all automation instances unless they are locally enabled using an override. + +***Number of Automation template instances Badge*** + +How many automation instances are using the template? The number in brackets indicates the number of disabled instances. + +# General settings + +1. **Title** + + Provide a title for this automation template. This title is for administrative purposes and helps in identifying the template. + +2. **Preference** + + Assign a reference to this automation template. This identifier is also for administrative purposes and assists in uniquely identifying the template. + +3. **Visibility** + + This option allows you to show or hide the automation template on the Automation Templates list. + + ***Note:*** If hidden, users won't be able to create new instances based on this template, but existing instances will still be available. + +4. **Internal Notes** + + Include any internal notes or information related to this automation template that will be visible on this template. + +5. **Status** + + This option is for enabling or disabling the template. + + ***Enabled:*** Allows instances of this template to be created. Enabling the template may also prompt the user to decide whether to enable all existing instances based on the template or only the template itself and not its instances. + + ***Disabled:*** Turns off the automation template and its instances. Users can still enable instances individually if needed. Disabling the template may prompt the user to decide whether to disable all existing instances based on the template or only the template itself and not its instances. + +6. **Tags** + + Add tags to this template for administrative purposes. Tags can help categorize and organize templates. + +7. **Available for tenants** + + Specify for which Moodle Workplace tenants this template should be available. Select one or more tenants to make the template accessible to specific groups. + +8. **Available in Course Categories** + + Choose the course categories where this template should be available. Select one or more categories to determine where users can create instances based on this template. + +![Pulse-automation-template - Edit settings](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/d4220218-02f2-4069-ace5-bcf05953250c) + + +### Conditions + +1. **Trigger** + + Choose the trigger events that will activate and be visible on the automation instances. You can select one or more of the following trigger options: + + ***Activity Completion:*** This automation will be triggered when an activity within the course is marked as completed. You will need to specify the activity within the automation instance. + + ***Course Completion:*** This automation will be triggered when the entire course is marked as completed, where this instance is used. + + ***Enrolments:*** This automation will be triggered when a user is enrolled in the course where this instance is located. + + ***Session:*** This automation will be triggered when a session is booked within the course. This trigger is only available within the course and should be selected within the automation instance. + + ***Cohort Membership:*** This automation will be triggered if the user is a member of one of the selected cohorts. + +2. **Trigger operator** + + Choose the operator that determines how the selected triggers are evaluated: + + ***Any:*** At least one of the selected triggers must occur to activate the automation. + + ***All:*** All of the selected triggers must occur simultaneously to activate the automation. + +![Pulse-automation-template - Condition](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/843795af-0972-490b-b078-cf69fc837eb1) + + +### Notifications + +1. **Sender** + + Determines how the selected triggers are evaluated. Choose the sender of the notification from the following options: + + **Course Teacher:** The notification will be sent from the course teacher (the first one assigned if there are several). If the user is not in any group, it falls back to the site support contact. Note that this is determined by capability, not by an actual role. + + **Group Teacher:** The notification will be sent from the non-editing teacher who is a member of the same group as the user (the first one assigned if there are several). If there's no non-editing teacher in the group, it falls back to the course teacher. Note that this is determined by capability, not by an actual role. + + **Tenant Role (Workplace Feature):** The notification will be sent from the user assigned to the specified role in the tenant (the first one assigned if there are several). If there's no user with the selected role, it falls back to the site support contact. Note that this is determined by capability, not by an actual role. + + **Custom:** If this option is selected, an additional setting for 'Sender Email' will become available. Here, you can enter a specific email address to be used as the sender. + + ***Sender email:*** You can enter a specific email address to be used as the sender. + +2. **Schedule** + + This scheduling allows you to control when the notification is delivered to its intended recipients. Choose the interval for sending notifications: + + **Once:** Send the notification only one time. + + **Daily:** Send the notification every day at the time selected below. + + **Weekly:** Send the notification every week on the day of the week and time of day selected below. + + **Monthly:** Send the notification every month on the day of the month and time of day selected below. + +3. **Delay** + + A notification that is postponed for a specific period before it is sent to the recipient. Choose the delay option for sending notifications. + + **None:** Send notifications immediately upon the condition being met, considering the schedule limitations (e.g., weekday or time of day). + + **Before X Days/Hours:** Send the notification a specified number of days/hours before the condition is met. Note that this is only possible for timed events, e.g., appointment sessions. + + **After X Days/Hours:** Send the notification a specified number of days/hours after the condition is met. This is possible for all conditions. + +4. **Limit Number of Notifications** + + This limit is typically imposed to prevent users from receiving an excessive number of notifications, which could be overwhelming or spammy. Enter a number to limit the total number of notifications sent. Enter "0" for no limit. This is only relevant if the schedule is not set to "Once." + +5. **Recipients** + + Select one or more roles that have the capability to receive notifications. By default, it's set for all graded roles, including students. Users selected here will be used in the query to determine who gets notifications. + +6. **CC** + + Select course context and user context roles that will receive the notification as a CC (Carbon Copy) alongside the main recipient. Course context roles determine users based on their enrollment in the course and membership in a group, while user context roles determine users based on their relationship to the recipient (assigned role in user). + +7. **BCC** + + Select course context and user context roles that will receive the notification as a BCC (Blind Carbon Copy) alongside the main recipient. Course context roles determine users based on their enrollment in the course and membership in a group, while user context roles determine users based on their relationship to the recipient (assigned role in user). + +8. **Subject** + + Refers to the title or headline that you would provide for an notification to briefly describe the content or purpose of the notification + +9. **Header Content** + + The context of email notifications refers to the information and elements displayed at the top of an email message before the main body of the email. This field supports filters and placeholders. + +10. **Static Content** + + The context of email notifications refers to the fixed or unchanging elements within the email that do not vary from one email to another. This field supports filters and placeholders. + +11. **Footer Content** + + The context of notifications refers to the information and elements placed at the bottom of a notification message. + +12. **Preview** + + Click this button to open a modal window that displays the notification, allowing you to select an example user to determine the content of the notification. + +![Pulse-automation-template - Notification](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/b46142ed-a4f5-445a-b2ef-98691cb3bcfd) + + +# Automation instances + +Based on the available automation templates within the current course, users with appropriate permissions can create automation instances. To create an automation instance, the user must select the automation template on the "Automation" page within a course and configure the instance accordingly. + +For each setting within an automation instance, the value from the template is used. If the user wants to deviate from the template's value, they can locally override it by toggling the switch to "override" and making local changes to the setting. + +Any changes made to the automation template will impact all instances where the setting has not been locally overridden. Automation instances inherit the same settings as the underlying automation template, with a few differences and exceptions. + +# Manage Automation instances lists + +![Pulse-automation-instances](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/3bc322af-a13f-489c-8699-187bce4d1097) + +***Select box*** + +You can choose an automation template from the following list to create an automation instance. + +***Add Automation Instances*** + +The 'Add automation instances' button that allows you to create automation instances in the selected automation template. + +***Manage templates*** + +The 'Manage Templates' button redirects you to the Manage Automation Templates listing page. + +***Sort*** + +It provides users with the ability to arrange and display a list of automation instances in a Alphabets order by the 'Preferences'. + +***Circle with Icon*** + +The icon represents the enabled actions in the automation template instances. The following actions are available: 'Notification,' 'Assignment,' 'Membership,' and 'Skills'. + +***Instances Title Name*** + +The title of the automation instances should provide a generic explanation of its purpose. + +***Pencil icon*** + +You can edit the automation instances title by clicking on the pencil icon next to it. + +***Notification Pills*** + +The pills provide additional important information about the automation instances. In this case, it explains that it's a notification. + +***Preferences*** + +This serves as the reference for the automation instances, providing a unique identifier. It will be part of the unique identifier for the automation instance. + +***Cog icon*** + +Click on this cog icon to edit the automation instances settings. + +***Duplicate icon*** + +Click on this copy icon to duplicate the specific automation instances. + +***Calendar icon*** + +Click on this Calendar icon to view the report page of the automation instances schedule. This report will display the 'Course full name', 'Message type,' 'Subject,' 'Full name,' 'Time created,' 'Scheduled time,' 'Status,' and you can also 'Download table data.' + +***Eye icon*** + +Use this toggle to enable or disable the automation instance locally. This will override the template's status. For example, even if the template is turned off, it can still be enabled here. + +***Delete icon*** + +Clicking on this delete icon will remove the specific automation instances from the automation template. + + +# General settings + +1. **Title** + + Provide a title for this automation template. This title is for administrative purposes and helps in identifying the template. + + *`Toggle button - If you enable the toggle button, the provided value will be applied for the 'title' in the instance; otherwise, the automation templates value of the 'title' will be applied.`* + +2. **Reference** + + Assign a reference to this automation instance. This identifier is also for administrative purposes and helps uniquely identify the instance. The 'reference' setting of this instance will have the prefix of its automation template's 'Reference'. + + *`Toggle button - If you enable the toggle button, the provided value will be applied for the 'reference' in the instance; otherwise, the automation templates value of the 'reference' will be applied.`* + +3. **Internal Notes** + + Include any internal notes or information related to this automation template that will be visible on this template. + + *`Toggle button - If you enable the toggle button, the provided value will be applied for the 'Internal Notes' in the instance; otherwise, the automation templates value of the 'Internal Notes' will be applied.`* + +4. **Status** + + This option is for enabling or disabling the template. + + ***Enabled:*** Allows instances of this template to be created and overrides the option, even if the automation template is enabled or disabled. + + ***Disabled:*** Disables the automation instances, regardless of whether the automation template is enabled or disabled. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'status' in the instance; otherwise, the automation templates option of the 'status' will be applied.`* + +5. **Tags** + + Add tags to this template for administrative purposes. Tags can help categorize and organize templates. + + *`Toggle button - If you enable the toggle button, the provided value will be applied for the 'tags' in the instance; otherwise, the automation templates value of the 'tags' will be applied.`* + +6. **Available for tenants** + + Specify for which Moodle Workplace tenants this template should be available. Select one or more tenants to make the template accessible to specific groups. + + *`Toggle button - If you enable the toggle button, the provided value will be applied for the 'Available for tenants' in the instance; otherwise, the automation templates value of the 'Available for tenants' will be applied.`* + +![Pulse-automation-instances - Edit settings](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/45e26035-f730-4e23-a275-3043b15d7879) + + +## Conditions + +1. **Trigger operator** + + Choose the operator that determines how the selected triggers are evaluated: + + ***Any:*** At least one of the selected triggers must occur to activate the automation. + + ***All:*** All of the selected triggers must occur simultaneously to activate the automation. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Trigger operator' in the instance; otherwise, the automation templates of the 'Trigger Operator' option will be applied.`* + +2. **Activity completion** + + This automation will be triggered when an activity within the course is marked as completed. You will need to specify the activity within the automation instance. The options for activity completion include: + + **Disabled:** Activity completion condition is disabled. + + **All:** Activity completion condition applies to all enrolled users. Enabling this option will make the 'Select Activities' option visible. + + **Upcoming:** Activity completion condition only applies to future enrollments. Enabling this option will make the 'Select Activities' option visible. + + *`'Select Activities: This setting allows you to choose from all available activities within your course that have completion configured. This selection determines which specific activities will trigger the automation when their completion conditions are met.`* + +3. **Enrolments** + + This automation will be triggered when a user is enrolled in the course where this instance is located. + + ***Disabled:*** Enrolment condition is disabled. + + ***All:*** Enrolment condition applies to all enrolments. + + ***Upcoming:*** Enrolment condition only applies to future enrolments. + +4. **Session Booking** + + This automation will be triggered when a session module is booked within the course. This trigger is only available within the course and should be selected within the automation instance. The options for session triggers include: + + ***Disabled:*** Session trigger is disabled. + + ***All:*** Session trigger applies to all enrolled users. Enabling this option will make the 'Session module' option visible. + + ***Upcoming:*** Session trigger only applies to future enrollments. Enabling this option will make the 'Session module' option visible. + + *`Session module: This setting allows you to choose the session module that will be associated with a session booking condition.`* + +5. **Cohort Membership** + + This automation will be triggered when a user belongs to one of the selected cohorts. The options for cohort membership include: + + ***Disabled:*** Cohort membership condition is disabled. + + ***All:*** Cohort membership condition applies to all enrolled users. Enabling this option will make the 'Cohort' option visible. + + ***Upcoming:*** Cohort membership condition only applies to future enrollments. Enabling this option will make the 'Cohort' option visible. + + *`Cohorts: This setting allows you to choose the cohorts. This selection determines which specific cohorts will trigger the automation when the users are assign on the cohorts.`* + +6. **Course completion** + + This automation will be triggered when the course is marked as completed, where this instance is used. The options for course completion include: + + **Disabled:** Course completion condition is disabled. + + **All:** Course completion condition applies to all enrolled users. + + **Upcoming:** Course completion condition only applies to future enrollments. + +![Pulse-automation-instances - Condition](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/00f33374-2235-495d-93c9-faed24a46aa7) + + +### Notifications + +1. **Sender** + + Determines how the selected triggers are evaluated. + + Choose the sender of the notification from the following options: + + **Course Teacher:** The notification will be sent from the course teacher (the first one assigned if there are several). If the user is not in any group, it falls back to the site support contact. Note that this is determined by capability, not by an actual role. + + **Group Teacher:** The notification will be sent from the non-editing teacher who is a member of the same group as the user (the first one assigned if there are several). If there's no non-editing teacher in the group, it falls back to the course teacher. Note that this is determined by capability, not by an actual role. + + **Tenant Role (Workplace Feature):** The notification will be sent from the user assigned to the specified role in the tenant (the first one assigned if there are several). If there's no user with the selected role, it falls back to the site support contact. Note that this is determined by capability, not by an actual role. + + **Custom:** If this option is selected, an additional setting for 'Sender Email' will become available. Here, you can enter a specific email address to be used as the sender. + + ***Sender email:*** You can enter a specific email address to be used as the sender. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'sender' option in the instance; otherwise, the automation templates of the 'sender' option will be applied.`* + +2. **Schedule** + + This scheduling allows you to control when the notification is delivered to its intended recipients. + + Choose the interval for sending notifications: + + **Once:** Send the notification only one time. + + **Daily:** Send the notification every day at the time selected below. + + **Weekly:** Send the notification every week on the day of the week and time of day selected below. + + **Monthly:** Send the notification every month on the day of the month and time of day selected below. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Schedule' in the instance; otherwise, the automation templates option of the 'Schedule' will be applied.`* + + +3. **Delay** + + A notification that is postponed for a specific period before it is sent to the recipient. + + Choose the delay option for sending notifications. + + **None:** Send notifications immediately upon the condition being met, considering the schedule limitations (e.g., weekday or time of day). + + **Before X Days/Hours:** Send the notification a specified number of days/hours before the condition is met. Note that this is only possible for timed events, e.g., appointment sessions. + + **After X Days/Hours:** Send the notification a specified number of days/hours after the condition is met. This is possible for all conditions. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Delay' in the instance; otherwise, the automation templates option of the 'Delay' will be applied.`* + +4. **Limit Number of Notifications** + + This limit is typically imposed to prevent users from receiving an excessive number of notifications, which could be overwhelming or spammy. Enter a number to limit the total number of notifications sent. Enter "0" for no limit. This is only relevant if the schedule is not set to "Once." + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Limit Number of Notifications' in the instance; otherwise, the automation templates option of the 'Limit Number of Notifications' will be applied.`* + +5. **Recipients** + + Select one or more roles that have the capability to receive notifications. By default, it's set for all graded roles, including students. Users selected here will be used in the query to determine who gets notifications. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Recipients' in the instance; otherwise, the automation templates option of the 'Recipients' will be applied.`* + +6. **CC** + + Select course context and user context roles that will receive the notification as a CC (Carbon Copy) to the main recipient. Course context roles determine users by enrolment in the course and membership of a group, while user context roles determine users by their relation to the recipient (assigned role in user). + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'CC' in the instance; otherwise, the automation templates option of the 'CC' will be applied.`* + +7. **BCC** + + Select course context and user context roles that will receive the notification as a BCC (Blind Carbon Copy) to the main recipient. Course context roles determine users by enrolment in the course and membership of a group, while user context roles determine users by their relation to the recipient (assigned role in user). + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'BCC' in the instance; otherwise, the automation templates option of the 'BCC' will be applied.`* + +8. **Subject** + + Refers to the title or headline that you would provide for an notification to briefly describe the content or purpose of the notification + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Subject' in the instance; otherwise, the automation templates option of the 'Subject' will be applied.`* + +9. **Header Content** + + The context of email notifications refers to the information and elements displayed at the top of an email message before the main body of the email. This field supports filters and placeholders. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Header Content' in the instance; otherwise, the automation templates option of the 'Header Content' will be applied.`* + +10. **Static Content** + + The context of email notifications refers to the fixed or unchanging elements within the email that do not vary from one email to another. This field supports filters and placeholders. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Static Content' in the instance; otherwise, the automation templates option of the 'Static Content' will be applied.`* + +11. **Dynamic Content** + + Select an activity within the course to add content below the static content. This is only available in the automation instance within the course. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Dynamic Content' in the instance; otherwise, the automation templates option of the 'Dynamic Content' will be applied.`* + + Choose the option on the Dynamic content: + + **None:** This option will disable the Dynamic content of the notification on the automation instances. + + ***Page:*** When you select the 'Page' activity option in Dynamic content, the 'Content Type' and 'Content Length' options will become visible. + + ***Book:*** When you select the 'Book' activity option in Dynamic content, the 'Content Type' and 'Content Length' and 'Chapters' options will become visible. + +12. **Content Type** + + Refers to the format of the content being used that helps to describe the type of data or information contained within a resource. Please note that this feature supports specific mod types, such as Page and Book. + + Choose the type of content to be added below the Dynamic content: + + **Description**: If this option is selected, the description of the chosen activity will be included in the body of the notification. + + **Content**: If this option is chosen, the content of the selected activity will be included in the notification body. + +14. **Content Length** + + Refers to the size or extent of a piece of content. + + Choose the content length to include in the notification + + **Teaser**: If chosen, only the first paragraph will be used, followed by a 'Read More' link. + + **Full, Linked**: If 'Full, Linked' is selected, the entire content shall be used with a link to the content provided after it. + + **Full, Not Linked**: If 'Full, Not Linked' is selected, the entire content shall be used without a link to the content afterward. + +15. ***Chapters*** + + Refer to the divisions or sections within a book that help organize and structure the content. + + Select which chapters of the chosen activity will be included in the notification body. To view the chapter content, select the specific chapter using the 'Chapters' option and the content using the 'Content' option for the 'Book' activity. + +15. **Footer Content** + + The context of notifications refers to the information and elements placed at the bottom of a notification message. This field supports filters and placeholders. + + *`Toggle button - If you enable the toggle button, the provided option will be applied for the 'Footer Content' in the instance; otherwise, the automation templates option of the 'Footer Content' will be applied.`* + +16. **Preview** + + Click this button to open a modal window that displays the notification, allowing you to select an example user to determine the content of the notification. + +![Pulse-automation-instances - Notification](https://github.com/bdecentgmbh/moodle-mod_pulse/assets/57126778/e16ca2ac-c191-49cb-8c90-3089e1f852e3) -# Translating this plugin -This Moodle plugin is shipped with an english language pack only. All translations into other languages must be managed through AMOS (https://lang.moodle.org) by what they will become part of Moodle's official language pack. -# Copyright -bdecent gmbh -bdecent.de diff --git a/actions/notification/amd/build/chaptersource.min.js b/actions/notification/amd/build/chaptersource.min.js new file mode 100644 index 0000000..8735c41 --- /dev/null +++ b/actions/notification/amd/build/chaptersource.min.js @@ -0,0 +1,12 @@ +/** + * Frameworks datasource. + * + * This module is compatible with core/form-autocomplete. + * + * @module tool_lp/frameworks_datasource + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("pulseaction_notification/chaptersource",["jquery","core/ajax","core/notification","core/modal_factory","core/fragment","core/str","core/modal_events"],(function($,Ajax,Notification,ModalFactory,Fragment,Str,ModalEvents){const previewModalBody=function(contextID){let userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(void 0!==window.tinyMCE)var params={contentheader:window.tinyMCE.get("id_pulsenotification_headercontent_editor").getContent(),contentstatic:window.tinyMCE.get("id_pulsenotification_staticcontent_editor").getContent(),contentfooter:window.tinyMCE.get("id_pulsenotification_footercontent_editor").getContent(),userid:userid};else params={contentheader:document.querySelector("#id_pulsenotification_headercontent_editoreditable").innerHTML,contentstatic:document.querySelector("#id_pulsenotification_staticcontent_editoreditable").innerHTML,contentfooter:document.querySelector("#id_pulsenotification_footercontent_editoreditable").innerHTML,userid:userid};var dynamicparams={};if(null!==document.querySelector("[name=pulsenotification_dynamiccontent]")){dynamicparams={contentdynamic:document.querySelector("[name=pulsenotification_dynamiccontent]").value,contenttype:document.querySelector("[name=pulsenotification_contenttype]").value,chapterid:document.querySelector("[name=pulsenotification_chapterid]").value,contentlength:document.querySelector("[name=pulsenotification_contentlength]").value};var form=document.forms["pulse-automation-template"],formdata=new FormData(form),formData={formdata:formdata=new URLSearchParams(formdata).toString()}}return Fragment.loadFragment("pulseaction_notification","preview_content",contextID,{...params,...dynamicparams,...formData})};return{processResults:function(selector,modules){return modules},transport:function(selector,query,success,failure){var mod=document.querySelector("#id_pulsenotification_dynamiccontent");Ajax.call([{methodname:"pulseaction_notification_get_chapters",args:{mod:mod.value}}])[0].then((function(result){success(result)})).fail(failure)},updateChapter:function(){const SELECTORS_chaperType="#id_pulsenotification_contenttype",SELECTORS_mod="#id_pulsenotification_dynamiccontent";document.querySelector(SELECTORS_chaperType).addEventListener("change",(e=>resetChapter())),document.querySelector(SELECTORS_mod).addEventListener("change",(e=>resetChapter()));var chapter=document.querySelector("#id_pulsenotification_chapterid");function resetChapter(){chapter.innerHTML="",chapter.value="";var event=new Event("change");chapter.dispatchEvent(event)}},previewNotification:function(contextid){var btn=document.querySelector('[name="pulsenotification_preview"]');null!==btn&&btn.addEventListener("click",(function(){var contextID;contextID=contextid,ModalFactory.create({title:Str.get_string("preview","pulseaction_notification"),body:previewModalBody(contextID),large:!0}).then((modal=>{modal.show(),modal.getRoot().on(ModalEvents.bodyRendered,(function(){modal.getRoot().get(0).querySelector("[name=userselector]").addEventListener("change",(e=>{e.preventDefault();var target=e.target;modal.setBody(previewModalBody(contextID,target.value))}))}))}))}))},reportModal:function(contextID){var btn=document.querySelectorAll('[data-target="view-content"]');null!==btn&&btn.forEach((element=>{element.addEventListener("click",(function(e){var target=e.target.closest("a"),instance=target.dataset.instanceid,userid=target.dataset.userid;!function(contextID,instance,userid){var params={instanceid:instance,userid:userid};ModalFactory.create({title:Str.get_string("preview","pulseaction_notification"),body:Fragment.loadFragment("pulseaction_notification","preview_instance_content",contextID,params),large:!0}).then((modal=>{modal.show()}))}(contextID,instance,userid)}))}))}}})); + +//# sourceMappingURL=chaptersource.min.js.map \ No newline at end of file diff --git a/actions/notification/amd/build/chaptersource.min.js.map b/actions/notification/amd/build/chaptersource.min.js.map new file mode 100644 index 0000000..9da4c17 --- /dev/null +++ b/actions/notification/amd/build/chaptersource.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"chaptersource.min.js","sources":["../src/chaptersource.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Frameworks datasource.\r\n *\r\n * This module is compatible with core/form-autocomplete.\r\n *\r\n * @module tool_lp/frameworks_datasource\r\n * @copyright 2016 Frédéric Massart - FMCorz.net\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine(['jquery', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/fragment', 'core/str', 'core/modal_events'],\r\n function($, Ajax, Notification, ModalFactory, Fragment, Str, ModalEvents) {\r\n\r\n\r\n const previewModalBody = function(contextID, userid=null) {\r\n\r\n if (window.tinyMCE !== undefined) {\r\n // editorPlugin = window.tinyMCE;\r\n var params = {\r\n contentheader: window.tinyMCE.get('id_pulsenotification_headercontent_editor').getContent(),\r\n contentstatic: window.tinyMCE.get('id_pulsenotification_staticcontent_editor').getContent(),\r\n contentfooter: window.tinyMCE.get('id_pulsenotification_footercontent_editor').getContent(),\r\n userid: userid\r\n };\r\n } else {\r\n // editorPlugin = document;\r\n var params = {\r\n contentheader: document.querySelector('#id_pulsenotification_headercontent_editoreditable').innerHTML,\r\n contentstatic: document.querySelector('#id_pulsenotification_staticcontent_editoreditable').innerHTML,\r\n contentfooter: document.querySelector('#id_pulsenotification_footercontent_editoreditable').innerHTML,\r\n userid: userid\r\n };\r\n }\r\n\r\n var dynamicparams = {};\r\n if (document.querySelector('[name=pulsenotification_dynamiccontent]') !== null) {\r\n dynamicparams = {\r\n contentdynamic: document.querySelector('[name=pulsenotification_dynamiccontent]').value,\r\n contenttype: document.querySelector('[name=pulsenotification_contenttype]').value,\r\n chapterid: document.querySelector('[name=pulsenotification_chapterid]').value,\r\n contentlength: document.querySelector('[name=pulsenotification_contentlength]').value,\r\n };\r\n\r\n // Get the form data.\r\n var form = document.forms['pulse-automation-template'];\r\n var formdata = new FormData(form);\r\n formdata = new URLSearchParams(formdata).toString();\r\n var formData = {\r\n formdata: formdata\r\n }\r\n }\r\n\r\n return Fragment.loadFragment('pulseaction_notification', 'preview_content', contextID, {...params, ...dynamicparams, ...formData});\r\n }\r\n\r\n const previewModal = function(contextID) {\r\n\r\n ModalFactory.create({\r\n title: Str.get_string('preview', 'pulseaction_notification'),\r\n body: previewModalBody(contextID),\r\n large: true,\r\n }).then((modal) => {\r\n modal.show();\r\n\r\n modal.getRoot().on(ModalEvents.bodyRendered, function() {\r\n modal.getRoot().get(0).querySelector('[name=userselector]').addEventListener('change', (e) => {\r\n e.preventDefault();\r\n var target = e.target;\r\n modal.setBody(previewModalBody(contextID, target.value));\r\n })\r\n })\r\n });\r\n };\r\n\r\n const notificationModal = function(contextID, instance, userid) {\r\n\r\n var params = {\r\n instanceid: instance,\r\n userid: userid\r\n };\r\n\r\n ModalFactory.create({\r\n title: Str.get_string('preview', 'pulseaction_notification'),\r\n body: Fragment.loadFragment('pulseaction_notification', 'preview_instance_content', contextID, params),\r\n large: true,\r\n }).then((modal) => {\r\n modal.show();\r\n\r\n /* modal.getRoot().on(ModalEvents.bodyRendered, function() {\r\n modal.getRoot().get(0).querySelector('[name=userselector]').addEventListener('change', (e) => {\r\n e.preventDefault();\r\n var target = e.target;\r\n modal.setBody(previewModalBody(contextID, target.value));\r\n })\r\n }) */\r\n });\r\n };\r\n\r\n return {\r\n\r\n processResults: function(selector, modules) {\r\n return modules;\r\n },\r\n\r\n transport: function(selector, query, success, failure) {\r\n\r\n var mod = document.querySelector(\"#id_pulsenotification_dynamiccontent\");\r\n\r\n var promise = Ajax.call([{\r\n methodname: 'pulseaction_notification_get_chapters',\r\n args: {mod: mod.value}\r\n }]);\r\n\r\n promise[0].then(function(result) {\r\n success(result);\r\n return;\r\n }).fail(failure);\r\n },\r\n\r\n updateChapter: function() {\r\n\r\n const SELECTORS = {\r\n chaperType : \"#id_pulsenotification_contenttype\",\r\n mod: \"#id_pulsenotification_dynamiccontent\"\r\n };\r\n\r\n document.querySelector(SELECTORS.chaperType).addEventListener(\"change\", (e) => resetChapter());\r\n document.querySelector(SELECTORS.mod).addEventListener(\"change\", (e) => resetChapter());\r\n var chapter = document.querySelector(\"#id_pulsenotification_chapterid\");\r\n\r\n function resetChapter() {\r\n chapter.innerHTML = '';\r\n chapter.value = '';\r\n var event = new Event('change');\r\n chapter.dispatchEvent(event);\r\n }\r\n },\r\n\r\n previewNotification: function(contextid) {\r\n var btn = document.querySelector('[name=\"pulsenotification_preview\"]');\r\n\r\n if (btn === null) {\r\n return;\r\n }\r\n\r\n btn.addEventListener('click', function() {\r\n previewModal(contextid);\r\n })\r\n },\r\n\r\n reportModal: function(contextID) {\r\n // View content.\r\n var btn = document.querySelectorAll('[data-target=\"view-content\"]');\r\n\r\n if (btn === null) {\r\n return;\r\n }\r\n\r\n btn.forEach((element) => {\r\n element.addEventListener('click', function(e) {\r\n\r\n var target = e.target.closest('a');\r\n\r\n var instance = target.dataset.instanceid;\r\n var userid = target.dataset.userid;\r\n\r\n notificationModal(contextID, instance, userid); // Notification modal.\r\n });\r\n });\r\n }\r\n };\r\n\r\n});\r\n"],"names":["define","$","Ajax","Notification","ModalFactory","Fragment","Str","ModalEvents","previewModalBody","contextID","userid","undefined","window","tinyMCE","params","contentheader","get","getContent","contentstatic","contentfooter","document","querySelector","innerHTML","dynamicparams","contentdynamic","value","contenttype","chapterid","contentlength","form","forms","formdata","FormData","formData","URLSearchParams","toString","loadFragment","processResults","selector","modules","transport","query","success","failure","mod","call","methodname","args","then","result","fail","updateChapter","SELECTORS","addEventListener","e","resetChapter","chapter","event","Event","dispatchEvent","previewNotification","contextid","btn","create","title","get_string","body","large","modal","show","getRoot","on","bodyRendered","preventDefault","target","setBody","reportModal","querySelectorAll","forEach","element","closest","instance","dataset","instanceid","notificationModal"],"mappings":"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"} \ No newline at end of file diff --git a/actions/notification/amd/build/notification.min.js b/actions/notification/amd/build/notification.min.js new file mode 100644 index 0000000..c192a23 --- /dev/null +++ b/actions/notification/amd/build/notification.min.js @@ -0,0 +1,3 @@ +define("pulseaction_notification/notification",["jquery","core/fragment"],(function($,Fragment){var contextID;const SELECTORS_chaperType="#id_pulsenotification_contenttype",SELECTORS_mod="#id_pulsenotification_dynamiccontent",updateChapter=method=>{var type=document.querySelector("#id_pulsenotification_contenttype").value,mod=document.querySelector("#id_pulsenotification_dynamiccontent"),chapter=document.querySelector("#id_pulsenotification_chapter");if(2!=parseInt(type))return!0;var params={mod:mod.value};Fragment.loadFragment("pulseaction_notification","update_chapters",contextID,params).then(((html,js)=>{chapter.innerHTML=html})).catch()};return{init:function(){},updateChapter:function(contextid){(contextid=>{contextID=contextid,document.querySelector(SELECTORS_chaperType).addEventListener("change",(e=>updateChapter())),document.querySelector(SELECTORS_mod).addEventListener("change",(e=>updateChapter()))})(contextid)}}})); + +//# sourceMappingURL=notification.min.js.map \ No newline at end of file diff --git a/actions/notification/amd/build/notification.min.js.map b/actions/notification/amd/build/notification.min.js.map new file mode 100644 index 0000000..3b65c42 --- /dev/null +++ b/actions/notification/amd/build/notification.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"notification.min.js","sources":["../src/notification.js"],"sourcesContent":["define('pulseaction_notification/notification', ['jquery', 'core/fragment'], function($, Fragment) {\r\n\r\n var contextID;\r\n\r\n const SELECTORS = {\r\n chaperType : \"#id_pulsenotification_contenttype\",\r\n mod: \"#id_pulsenotification_dynamiccontent\"\r\n };\r\n\r\n const addChapterEventListners = (contextid) => {\r\n\r\n contextID = contextid\r\n\r\n document.querySelector(SELECTORS.chaperType).addEventListener(\"change\", (e) => updateChapter());\r\n document.querySelector(SELECTORS.mod).addEventListener(\"change\", (e) => updateChapter());\r\n }\r\n\r\n const updateChapter = (method) => {\r\n var type = document.querySelector(\"#id_pulsenotification_contenttype\").value;\r\n var mod = document.querySelector(\"#id_pulsenotification_dynamiccontent\");\r\n var chapter = document.querySelector(\"#id_pulsenotification_chapter\");\r\n\r\n if (parseInt(type) != 2) {\r\n return true;\r\n }\r\n\r\n var params = {mod: mod.value};\r\n\r\n // TODO: Loading icon near.\r\n Fragment.loadFragment('pulseaction_notification', 'update_chapters', contextID, params).then((html, js) => {\r\n chapter.innerHTML = html;\r\n }).catch();\r\n }\r\n\r\n\r\n return {\r\n\r\n init: function() {\r\n\r\n },\r\n\r\n updateChapter: function(contextid) {\r\n addChapterEventListners(contextid);\r\n }\r\n }\r\n})\r\n"],"names":["define","$","Fragment","contextID","SELECTORS","updateChapter","method","type","document","querySelector","value","mod","chapter","parseInt","params","loadFragment","then","html","js","innerHTML","catch","init","contextid","addEventListener","e","addChapterEventListners"],"mappings":"AAAAA,+CAAgD,CAAC,SAAU,kBAAkB,SAASC,EAAGC,cAEjFC,gBAEEC,qBACW,oCADXA,cAEG,uCAWHC,cAAiBC,aACfC,KAAOC,SAASC,cAAc,qCAAqCC,MACnEC,IAAMH,SAASC,cAAc,wCAC7BG,QAAUJ,SAASC,cAAc,oCAEf,GAAlBI,SAASN,aACF,MAGPO,OAAS,CAACH,IAAKA,IAAID,OAGvBR,SAASa,aAAa,2BAA4B,kBAAmBZ,UAAWW,QAAQE,MAAK,CAACC,KAAMC,MAChGN,QAAQO,UAAYF,QACrBG,eAIA,CAEHC,KAAM,aAINhB,cAAe,SAASiB,WAhCKA,CAAAA,YAE7BnB,UAAYmB,UAEZd,SAASC,cAAcL,sBAAsBmB,iBAAiB,UAAWC,GAAMnB,kBAC/EG,SAASC,cAAcL,eAAemB,iBAAiB,UAAWC,GAAMnB,mBA4BpEoB,CAAwBH"} \ No newline at end of file diff --git a/actions/notification/amd/src/chaptersource.js b/actions/notification/amd/src/chaptersource.js new file mode 100644 index 0000000..fb1b673 --- /dev/null +++ b/actions/notification/amd/src/chaptersource.js @@ -0,0 +1,188 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Frameworks datasource. + * + * This module is compatible with core/form-autocomplete. + * + * @module tool_lp/frameworks_datasource + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/fragment', 'core/str', 'core/modal_events'], + function($, Ajax, Notification, ModalFactory, Fragment, Str, ModalEvents) { + + const previewModalBody = function(contextID, userid = null) { + + var params; + if (window.tinyMCE !== undefined) { + // EditorPlugin = window.tinyMCE; + params = { + contentheader: window.tinyMCE.get('id_pulsenotification_headercontent_editor').getContent(), + contentstatic: window.tinyMCE.get('id_pulsenotification_staticcontent_editor').getContent(), + contentfooter: window.tinyMCE.get('id_pulsenotification_footercontent_editor').getContent(), + userid: userid + }; + } else { + // EditorPlugin = document; + params = { + contentheader: document.querySelector('#id_pulsenotification_headercontent_editoreditable').innerHTML, + contentstatic: document.querySelector('#id_pulsenotification_staticcontent_editoreditable').innerHTML, + contentfooter: document.querySelector('#id_pulsenotification_footercontent_editoreditable').innerHTML, + userid: userid + }; + } + + var dynamicparams = {}; + var formData; + if (document.querySelector('[name=pulsenotification_dynamiccontent]') !== null) { + dynamicparams = { + contentdynamic: document.querySelector('[name=pulsenotification_dynamiccontent]').value, + contenttype: document.querySelector('[name=pulsenotification_contenttype]').value, + chapterid: document.querySelector('[name=pulsenotification_chapterid]').value, + contentlength: document.querySelector('[name=pulsenotification_contentlength]').value, + }; + + // Get the form data. + var form = document.forms['pulse-automation-template']; + var formdata = new FormData(form); + formdata = new URLSearchParams(formdata).toString(); + formData = { + formdata: formdata + }; + } + var finalParams = {...params, ...dynamicparams, ...formData}; + + return Fragment.loadFragment('pulseaction_notification', 'preview_content', contextID, finalParams); + }; + + const previewModal = function(contextID) { + + ModalFactory.create({ + title: Str.get_string('preview', 'pulseaction_notification'), + body: previewModalBody(contextID), + large: true, + }).then((modal) => { + modal.show(); + + modal.getRoot().on(ModalEvents.bodyRendered, function() { + modal.getRoot().get(0).querySelector('[name=userselector]').addEventListener('change', (e) => { + e.preventDefault(); + var target = e.target; + modal.setBody(previewModalBody(contextID, target.value)); + }); + }); + + return; + }).catch(); + }; + + const notificationModal = function(contextID, instance, userid) { + + var params = { + instanceid: instance, + userid: userid + }; + + ModalFactory.create({ + title: Str.get_string('preview', 'pulseaction_notification'), + body: Fragment.loadFragment('pulseaction_notification', 'preview_instance_content', contextID, params), + large: true, + }).then((modal) => { + modal.show(); + return; + }).catch(); + }; + + return { + + processResults: function(selector, modules) { + return modules; + }, + + transport: function(selector, query, success, failure) { + + var mod = document.querySelector("#id_pulsenotification_dynamiccontent"); + + var promise = Ajax.call([{ + methodname: 'pulseaction_notification_get_chapters', + args: {mod: mod.value} + }]); + + promise[0].then(function(result) { + success(result); + return; + }).fail(failure); + }, + + updateChapter: function() { + + const SELECTORS = { + chaperType: "#id_pulsenotification_contenttype", + mod: "#id_pulsenotification_dynamiccontent" + }; + + document.querySelector(SELECTORS.chaperType).addEventListener("change", () => resetChapter()); + document.querySelector(SELECTORS.mod).addEventListener("change", () => resetChapter()); + var chapter = document.querySelector("#id_pulsenotification_chapterid"); + + /** + * + */ + function resetChapter() { + chapter.innerHTML = ''; + chapter.value = ''; + var event = new Event('change'); + chapter.dispatchEvent(event); + } + }, + + previewNotification: function(contextid) { + var btn = document.querySelector('[name="pulsenotification_preview"]'); + + if (btn === null) { + return; + } + + btn.addEventListener('click', function() { + previewModal(contextid); + }); + }, + + reportModal: function(contextID) { + // View content. + var btn = document.querySelectorAll('[data-target="view-content"]'); + + if (btn === null) { + return; + } + + btn.forEach((element) => { + element.addEventListener('click', function(e) { + + var target = e.target.closest('a'); + + var instance = target.dataset.instanceid; + var userid = target.dataset.userid; + + notificationModal(contextID, instance, userid); // Notification modal. + }); + }); + } + }; + +}); diff --git a/actions/notification/classes/actionform.php b/actions/notification/classes/actionform.php new file mode 100644 index 0000000..de4aed1 --- /dev/null +++ b/actions/notification/classes/actionform.php @@ -0,0 +1,720 @@ +. + +/** + * Notification pulse action form. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification; + +use html_writer; +use moodle_exception; +use DateTime; +use DatePeriod; +use mod_pulse\automation\helper; +use pulseaction_notification\notification; +use pulseaction_notification\schedule; + +/** + * Notification action form, contains important method and basic plugin details. + */ +class actionform extends \mod_pulse\automation\action_base { + + /** + * Shortname for the config used in the form field. + * + * @return string + */ + public function config_shortname() { + return 'pulsenotification'; + } + + /** + * Get the icon for this component, displayed on the instances list on the course autotemplates sections. + * + * @return string + */ + public function get_action_icon() { + global $OUTPUT; + return $OUTPUT->pix_icon("i/notifications", get_string('notifications')); + } + + /** + * Delete notification instances and schedule data for this instance. + * + * @param int $instanceid + * @return void + */ + public function delete_instance_action(int $instanceid) { + global $DB; + + parent::delete_instance_action($instanceid); + $instancetable = 'pulseaction_notification_sch'; + return $DB->delete_records($instancetable, ['instanceid' => $instanceid]); + } + + /** + * Prepare editor fileareas. + * + * @param stdclass $data Instance/Templates data of the notification. + * @param \context $context + * @return void + */ + public function prepare_editor_fileareas(&$data, \context $context) { + + $data = (object) ($data ?: ['id' => 0]); // Create empty data set if empty. + + $context = \context_system::instance(); + $templateid = $data->templateid ?? $data->id; + + // Support for the instance form. use the course context to prepare and update editor incase it's override in the instance. + if (isset($data->courseid) && isset($data->instanceid)) { + $prefix = '_instance'; + } + // List of editors need to prepare for forms. + $editor = [ + "pulsenotification_headercontent", + "pulsenotification_staticcontent", + "pulsenotification_footercontent" + ]; + + foreach ($editor as $field) { + // Create empty data set for the new template. + if (!isset($data->$field)) { + $data->$field = ''; + $data->{$field."format"} = editors_get_preferred_format(); + } + + $id = isset($data->instanceid) && isset($data->override[$field.'_editor']) ? $data->instanceid : $templateid; + $filearea = isset($prefix) && isset($data->override[$field.'_editor']) ? $field.$prefix : $field; + + $data = file_prepare_standard_editor( + $data, $field, $this->get_editor_options($context), $context, 'mod_pulse', $filearea, $id + ); + } + + } + + /** + * Prepare editor fileareas. + * + * @param stdclass $data Instance/Templates data of the notification. + * @param \context $context + * @return void + */ + public function postupdate_editor_fileareas(&$data, \context $context) { + $data = (object) $data; + + $context = \context_system::instance(); + $templateid = $data->templateid ?? $data->id; + + $editor = [ + "pulsenotification_headercontent", + "pulsenotification_staticcontent", + "pulsenotification_footercontent" + ]; + + if (isset($data->courseid) && isset($data->instanceid) ) { + $prefix = '_instance'; + } + + foreach ($editor as $field) { + + if (!isset($data->$field) && !isset($data->{$field.'_editor'})) { + continue; + } + + $id = $data->instanceid ?? $templateid; + $filearea = isset($prefix) ? $field.$prefix : $field; + + $data = file_postupdate_standard_editor( + $data, $field, $this->get_editor_options($context), $context, 'mod_pulse', $filearea, $id + ); + } + } + + /** + * Get text editor options to manage files. + * + * @param \stdclass $context + * @return void + */ + protected function get_editor_options($context=null) { + global $PAGE; + + return [ + 'trusttext' => true, + 'subdirs' => true, + 'maxfiles' => 50, + 'context' => $context ?: $PAGE->context + ]; + } + + /** + * Delete the notification instance. + * + * @param int $templateid + * @return void + */ + public function delete_template_action($templateid) { + global $DB; + + $instances = $this->get_template_instances($templateid); + // Remove its instances and schedules when the template is deleted. + foreach ($instances as $instanceid => $instance) { + $DB->delete_records('pulseaction_notification_ins', ['instanceid' => $instanceid]); + $DB->delete_records('pulseaction_notification_sch', ['instanceid' => $instanceid]); + } + + return $DB->delete_records('pulseaction_notification', ['templateid' => $templateid]); + } + + + /** + * Action is triggered for the instance. when triggered notification will create a schedule for the triggered users. + * + * Create the notification instance and initiate the schedule for this instance. + * + * @param stdclass $instancedata + * @param int $userid + * @param int $expectedtime + * @param bool $newuser + * + * @return void + */ + public function trigger_action($instancedata, $userid, $expectedtime=null, $newuser=false) { + + $notification = notification::instance($instancedata->pulsenotification_id); + $notificationinstance = helper::filter_record_byprefix($instancedata, $this->config_shortname()); + + $notification->set_notification_data($notificationinstance, $instancedata); + + // Create a schedule for user. This method verify the user activity completion before creating schedules. + $notification->create_schedule_foruser($userid, '', null, $expectedtime ?? null, $newuser); + + // Send the scheduled notifications for this user. + schedule::instance()->send_scheduled_notification($userid); + } + + /** + * Remove the user schedules when the user is deleted. + * + * Observe the events, triggered from the main pulse. + * + * @param stdclass $instancedata Automation Instance data. + * @param string $method Name of the triggered event. + * @param stdclass $eventdata Triggered event data. + * + * @return void + */ + public function trigger_action_event($instancedata, $method, $eventdata) { + + if ($method == 'user_enrolment_deleted') { + + $notificationid = $instancedata->actions['notification']['id']; + $notification = notification::instance($notificationid); + $notification->set_notification_data($instancedata->actions['notification'], $instancedata); + $userid = $eventdata->relateduserid; + + $notification->remove_user_schedules($userid); + } + } + + /** + * Get the notification record to attach the template create form. + * + * @param int $templateid + * @return stdclass + */ + public function get_data_fortemplate($templateid) { + global $DB; + // Notification data for template. + $actiondata = $DB->get_record('pulseaction_notification', ['templateid' => $templateid]); + return $actiondata; + } + + /** + * Get the notification instance record. + * + * @param int $instanceid + * @return stdclass Data of the notification instance. + */ + public function get_data_forinstance($instanceid) { + global $DB; + $instancedata = $DB->get_record('pulseaction_notification_ins', ['instanceid' => $instanceid]); + return $instancedata; + } + + /** + * Decode the json encoded notification data. + * + * @param array $actiondata + * @return void + */ + public function update_encode_data(&$actiondata) { + + $actiondata = (array) $actiondata; + + $actiondata['recipients'] = json_decode($actiondata['recipients']); + $actiondata['bcc'] = json_decode($actiondata['bcc']); + $actiondata['cc'] = json_decode($actiondata['cc']); + $actiondata['suppress'] = isset($actiondata['suppress']) ? json_decode($actiondata['suppress']) : []; + $actiondata['notifyinterval'] = json_decode($actiondata['notifyinterval'], true); + } + + /** + * Encode the array fields to json type. + * + * @param array $actiondata + * @return void + */ + protected function update_data_structure(&$actiondata) { + + // Testing the action data. + array_walk($actiondata, function(&$value) { + if (is_array($value)) { + $value = json_encode($value); + } + }); + } + + /** + * Recreate the schedule for the notification instance, It mostly used when the template is updated. + * + * @param int $templateid Updated temmplate ID. + * + * @return void + */ + protected function recreate_instance_schedules(int $templateid) { + global $DB; + + $instances = $this->get_template_instances($templateid); + + foreach ($instances as $instanceid => $instance) { + $notification = $DB->get_field('pulseaction_notification', 'id', ['instanceid' => $instanceid]); + notification::instance($notification->id)->recreate_schedule_forinstance(); + } + + } + + /** + * Save the template config. + * + * @param stdclass $record + * @param string $component + * + * @return bool + */ + public function process_save($record, $component) { + global $DB; + + $context = \context_system::instance(); + + $this->postupdate_editor_fileareas($record, $context); + + // Filter the current action data from the templates data by its shortname. + $actiondata = $this->filter_action_data((array) $record); + + $actiondata->templateid = $record->templateid; + + if (!empty($actiondata)) { + + // Update the data strucured before save. + $this->update_data_structure($actiondata); + + try { + // In moodle, the main table should be the name of the component. + // Therefore, generate the table name based on the component name. + $tablename = 'pulseaction_'. $component; + // Get the record by using the templateid, one instance is allowed for each template. + // Manage the action component data for this template. + if ($notification = $DB->get_record($tablename, ['templateid' => $record->templateid])) { + $actiondata->id = $notification->id; + // Update the latest data into the action component. + $DB->update_record($tablename, $actiondata); + } else { + // Create new instance for this tempalte. + $DB->insert_record($tablename, $actiondata); + } + + // Recreate the schedules for the instance. + $templateid = $actiondata->templateid; + $this->recreate_instance_schedules($templateid); + + } catch (\Exception $e) { + // Throw an error incase of issue with manage the data update. + throw new \moodle_exception('actiondatanotsave', $component); + } + } + return true; + } + + /** + * Save the submitted instance data for the notification action. Update the array values to json. + * After insert/update the data to DB then trigger the notification schedule for the instance course. + * + * @param int $instanceid + * @param stdclass $record + * @return bool + */ + public function process_instance_save($instanceid, $record) { + global $DB; + + // Filter the current action data from the templates data by its shortname. + $actiondata = $this->filter_action_data((array) $record); + // Update the data strucured before save. + $this->update_data_structure($actiondata); + $actiondata->instanceid = $instanceid; + + try { + // In moodle, the main table should be the name of the component. + // Therefore, generate the table name based on the component name. + $tablename = 'pulseaction_notification_ins'; + // Get the record by using the templateid, one instance is allowed for each template. + // Manage the action component data for this template. + if (isset($instanceid) && $notifyinstance = $DB->get_record($tablename, ['instanceid' => $instanceid])) { + $actiondata->id = $notifyinstance->id; + + $notificationinstance = $actiondata->id; + // Update the latest data into the action component. + $DB->update_record($tablename, $actiondata); + } else { + // Create new instance for this tempalte. + $notificationinstance = $DB->insert_record($tablename, $actiondata); + } + // TODO: Create a schedules based on receipents role. + notification::instance($notificationinstance)->create_schedule_forinstance(); + } catch (\Exception $e) { + // Throw an error incase of issue with manage the data update. + throw new \moodle_exception('actiondatanotsave', 'pulseaction_notification'); + } + + return true; + } + + /** + * Default override elements. + * + * @return array + */ + public function default_override_elements() { + // List of pulse notification elements those are available in only instances. + return [ + 'pulsenotification_suppress', + 'pulsenotification_suppressoperator', + 'pulsenotification_dynamiccontent', + 'pulsenotification_contenttype', + 'pulsenotification_chapterid', + 'pulsenotification_contentlength', + ]; + } + + /** + * Load the notification elements for the instance form. + * + * @param moodle_form $mform + * @param actionform $forminstance + * @return void + */ + public function load_instance_form(&$mform, $forminstance) { + global $CFG, $PAGE, $DB; + + require_once($CFG->dirroot.'/lib/modinfolib.php'); + + $this->load_global_form($mform, $forminstance); + + // Dynamic Content Group + // Add 'dynamic_content' element with all activities in the course. + $courseid = $forminstance->get_customdata('courseid') ?? ''; + $modinfo = \course_modinfo::instance($courseid); + + // Include the suppress activity settings for the instance. + $completion = new \completion_info(get_course($courseid)); + $activities = $completion->get_activities(); + array_walk($activities, function(&$value) { + $value = $value->name; + }); + + $suppress = $mform->createElement('autocomplete', 'pulsenotification_suppress', + get_string('suppressmodule', 'pulseaction_notification'), $activities, array('multiple' => 'multiple')); + + $mform->insertElementBefore($suppress, 'pulsenotification_notifylimit'); + $mform->addHelpButton('pulsenotification_suppress', 'suppressmodule', 'pulseaction_notification'); + + // Operator element. + $operators = [ + \mod_pulse\automation\action_base::OPERATOR_ALL => get_string('all', 'pulse'), + \mod_pulse\automation\action_base::OPERATOR_ANY => get_string('any', 'pulse'), + ]; + $suppressopertor = $mform->createElement('select', 'pulsenotification_suppressoperator', + get_string('suppressoperator', 'pulseaction_notification'), $operators); + $mform->setDefault('suppressoperator', \mod_pulse\automation\action_base::OPERATOR_ANY); + $mform->insertElementBefore($suppressopertor, 'pulsenotification_notifylimit'); + $mform->addHelpButton('pulsenotification_suppressoperator', 'suppressoperator', 'pulseaction_notification'); + + $modules = [0 => get_string('none')]; + $books = $modinfo->get_instances_of('book'); + $pages = $modinfo->get_instances_of('page'); + $list = array_merge($books, $pages); + foreach ($list as $page) { + $modules[$page->id] = $page->get_formatted_name(); + } + + $dynamic = $mform->createElement('select', 'pulsenotification_dynamiccontent', + get_string('dynamiccontent', 'pulseaction_notification'), $modules); + $mform->insertElementBefore($dynamic, 'pulsenotification_footercontent_editor'); + $mform->addHelpButton('pulsenotification_dynamiccontent', 'dynamiccontent', 'pulseaction_notification'); + + // Add 'content_type' element with the following options. + $contenttypeoptions = array( + notification::DYNAMIC_DESCRIPTION => get_string('description', 'pulseaction_notification'), + notification::DYNAMIC_CONTENT => get_string('content', 'pulseaction_notification'), + ); + $dynamic2 = $mform->createElement('select', 'pulsenotification_contenttype', + get_string('contenttype', 'pulseaction_notification'), $contenttypeoptions); + $mform->insertElementBefore($dynamic2, 'pulsenotification_footercontent_editor'); + $mform->hideIf('pulsenotification_contenttype', 'pulsenotification_dynamiccontent', 'eq', 0); + $mform->addHelpButton('pulsenotification_contenttype', 'contenttype', 'pulseaction_notification'); + + // Load Chapters for selected book. + $instanceid = $forminstance->get_customdata('instanceid'); + $cmid = $instanceid ? $DB->get_field('pulseaction_notification_ins', 'dynamiccontent', ['instanceid' => $instanceid]) : ''; + if (!empty($cmid)) { + $sql = 'SELECT bc.id, bc.title FROM {course_modules} cm + JOIN {book_chapters} bc ON bc.bookid = cm.instance + WHERE cm.id = :cmid'; + $chapters = $DB->get_records_sql_menu($sql, ['cmid' => $cmid]); + } + $options['ajax'] = 'pulseaction_notification/chaptersource'; + $chapter = $mform->createElement('autocomplete', 'pulsenotification_chapterid', + get_string('chapters', 'pulseaction_notification'), $chapters ?? [], $options); + $mform->insertElementBefore($chapter, 'pulsenotification_footercontent_editor'); + $mform->addHelpButton('pulsenotification_chapterid', 'chapters', 'pulseaction_notification'); + $mform->hideIf('pulsenotification_chapterid', 'pulsenotification_dynamiccontent', 'eq', 0); + foreach ($pages as $page) { + $mform->hideIf('pulsenotification_chapterid', 'pulsenotification_dynamiccontent', 'eq', $page->id); + } + + // Content Length Group. + $contentlengthoptions = array( + notification::LENGTH_TEASER => get_string('teaser', 'pulseaction_notification'), + notification::LENGTH_LINKED => get_string('full_linked', 'pulseaction_notification'), + notification::LENGTH_NOTLINKED => get_string('full_not_linked', 'pulseaction_notification'), + ); + $dynamic3 = $mform->createElement('select', 'pulsenotification_contentlength', + get_string('contentlength', 'pulseaction_notification'), $contentlengthoptions); + $mform->insertElementBefore($dynamic3, 'pulsenotification_footercontent_editor'); + $mform->addHelpButton('pulsenotification_contentlength', 'contentlength', 'pulseaction_notification'); + + $mform->hideIf('pulsenotification_contentlength', 'pulsenotification_dynamiccontent', 'eq', 0); + + asort($mform->_elementIndex); + + $PAGE->requires->js_call_amd('pulseaction_notification/chaptersource', 'updateChapter', + ['contextid' => $PAGE->context->id] + ); + } + + /** + * Global form elements for notification action. + * + * @param moodle_form $mform + * @param \automation_instance_form $forminstance + * @return void + */ + public function load_global_form(&$mform, $forminstance) { + global $CFG, $PAGE; + + require_once($CFG->dirroot.'/course/lib.php'); + + // Sender Group. + $senderoptions = array( + notification::SENDERCOURSETEACHER => get_string('courseteacher', 'pulseaction_notification'), + notification::SENDERGROUPTEACHER => get_string('groupteacher', 'pulseaction_notification'), + notification::SENDERTENANTROLE => get_string('tenantrole', 'pulseaction_notification'), + notification::SENDERCUSTOM => get_string('custom', 'pulseaction_notification') + ); + $mform->addElement('select', 'pulsenotification_sender', get_string('sender', 'pulseaction_notification'), $senderoptions); + $mform->addHelpButton('pulsenotification_sender', 'sender', 'pulseaction_notification'); + + // Add additional settings for the 'custom' option, if selected. + $mform->addElement('text', 'pulsenotification_senderemail', get_string('senderemail', 'pulseaction_notification')); + $mform->setType('pulsenotification_senderemail', PARAM_EMAIL); + $mform->hideIf('pulsenotification_senderemail', 'pulsenotification_sender', 'neq', notification::SENDERCUSTOM); + + $interval = []; + // Schedule Group. + $intervaloptions = array( + notification::INTERVALONCE => get_string('once', 'pulseaction_notification'), + notification::INTERVALDAILY => get_string('daily', 'pulseaction_notification'), + notification::INTERVALWEEKLY => get_string('weekly', 'pulseaction_notification'), + notification::INTERVALMONTHLY => get_string('monthly', 'pulseaction_notification'), + ); + $interval[] =& $mform->createElement('select', 'pulsenotification_notifyinterval[interval]', + get_string('interval', 'pulseaction_notification'), $intervaloptions); + + // Add additional settings based on the selected interval. + $dayweeks = array( + 'monday' => get_string('monday', 'pulseaction_notification'), + 'tuesday' => get_string('tuesday', 'pulseaction_notification'), + 'wednesday' => get_string('wednesday', 'pulseaction_notification'), + 'thursday' => get_string('thursday', 'pulseaction_notification'), + 'friday' => get_string('friday', 'pulseaction_notification'), + 'saturday' => get_string('saturday', 'pulseaction_notification'), + 'sunday' => get_string('sunday', 'pulseaction_notification'), + ); + + // Add 'day_of_week' element if 'weekly' is selected in the 'interval' element. + $interval[] =& $mform->createElement('select', 'pulsenotification_notifyinterval[weekday]', + get_string('interval', 'pulseaction_notification'), $dayweeks); + $mform->hideIf('pulsenotification_notifyinterval[weekday]', 'pulsenotification_notifyinterval[interval]', + 'neq', notification::INTERVALWEEKLY); + + $dates = range(1, 31); + // Add 'day_of_month' element if 'monthly' is selected in the 'interval' element. + $interval[] =& $mform->createElement('select', 'pulsenotification_notifyinterval[monthdate]', + get_string('interval', 'pulseaction_notification'), $dates); + $mform->hideIf('pulsenotification_notifyinterval[monthdate]', 'pulsenotification_notifyinterval[interval]', + 'neq', notification::INTERVALMONTHLY); + + // Time to send notification. + $dates = $this->get_times(); + // Add 'time_of_day' element. + // Add 'day_of_month' element if 'monthly' is selected in the 'interval' element. + $interval[] =& $mform->createElement('select', 'pulsenotification_notifyinterval[time]', + get_string('interval', 'pulseaction_notification'), $dates); + $mform->hideIf('pulsenotification_notifyinterval[time]', 'pulsenotification_notifyinterval[interval]', + 'eq', notification::INTERVALONCE); + + // Notification interval button groups. + $mform->addGroup($interval, 'pulsenotification_notifyinterval', + get_string('interval', 'pulseaction_notification'), array(' '), false); + $mform->addHelpButton('pulsenotification_notifyinterval', 'interval', 'pulseaction_notification'); + + // Notification delay. + $delayoptions = array( + notification::DELAYNONE => get_string('none', 'pulseaction_notification'), + notification::DELAYBEFORE => get_string('before', 'pulseaction_notification'), + notification::DELAYAFTER => get_string('after', 'pulseaction_notification'), + ); + $mform->addElement('select', 'pulsenotification_notifydelay', + get_string('delay', 'pulseaction_notification'), $delayoptions); + $mform->setDefault('delay', 'none'); + $mform->addHelpButton('pulsenotification_notifydelay', 'delay', 'pulseaction_notification'); + + // Delay duration. + $mform->addElement('duration', 'pulsenotification_delayduration', get_string('delayduraion', 'pulseaction_notification')); + $mform->hideIf('pulsenotification_delayduration', 'pulsenotification_notifydelay', 'eq', notification::DELAYNONE); + $mform->addHelpButton('pulsenotification_delayduration', 'delayduraion', 'pulseaction_notification'); + + // Limit no of notifications Group. + $mform->addElement('text', 'pulsenotification_notifylimit', get_string('limit', 'pulseaction_notification')); + $mform->setType('pulsenotification_notifylimit', PARAM_INT); + $mform->addHelpButton('pulsenotification_notifylimit', 'limit', 'pulseaction_notification'); + $mform->hideIf('pulsenotification_notifylimit', 'pulsenotification_notifyinterval[interval]', + 'eq', notification::INTERVALONCE); + + // Recipients Group. + // Add 'recipients' element with all roles that can receive notifications. + $roles = get_roles_with_capability('pulseaction/notification:receivenotification'); + $rolenames = role_fix_names($roles); + $roleoptions = array_combine(array_column($rolenames, 'id'), array_column($rolenames, 'localname')); + $mform->addElement('autocomplete', 'pulsenotification_recipients', + get_string('recipients', 'pulseaction_notification'), $roleoptions, array('multiple' => 'multiple')); + $mform->addHelpButton('pulsenotification_recipients', 'recipients', 'pulseaction_notification'); + + // CC Group. + // Add 'cc' element with all course context and user context roles. + $courseroles = $forminstance->course_roles(); + $mform->addElement('autocomplete', 'pulsenotification_cc', + get_string('ccrecipients', 'pulseaction_notification'), $courseroles, array('multiple' => 'multiple')); + $mform->addHelpButton('pulsenotification_cc', 'ccrecipients', 'pulseaction_notification'); + + // Set BCC. + $mform->addElement('autocomplete', 'pulsenotification_bcc', + get_string('bccrecipients', 'pulseaction_notification'), $courseroles, array('multiple' => 'multiple')); + $mform->addHelpButton('pulsenotification_bcc', 'bccrecipients', 'pulseaction_notification'); + + // Subject. + $mform->addElement('text', 'pulsenotification_subject', + get_string('subject', 'pulseaction_notification'), ['size' => 100]); + $mform->setType('pulsenotification_subject', PARAM_TEXT); + $mform->addHelpButton('pulsenotification_subject', 'subject', 'pulseaction_notification'); + + $context = \context_system::instance(); + $mform->addElement('editor', 'pulsenotification_headercontent_editor', + get_string('headercontent', 'pulseaction_notification'), + ['class' => 'fitem_id_templatevars_editor'], + $this->get_editor_options($context) + ); + $mform->addHelpButton('pulsenotification_headercontent_editor', 'headercontent', 'pulseaction_notification'); + $forminstance->pulse_email_placeholders($mform); + + // Statecontent editor. + $mform->addElement('editor', 'pulsenotification_staticcontent_editor', + get_string('staticcontent', 'pulseaction_notification'), + ['class' => 'fitem_id_templatevars_editor'], + $this->get_editor_options($context) + ); + $mform->addHelpButton('pulsenotification_staticcontent_editor', 'staticcontent', 'pulseaction_notification'); + $forminstance->pulse_email_placeholders($mform); + + // Footer Content. + $mform->addElement('editor', 'pulsenotification_footercontent_editor', + get_string('footercontent', 'pulseaction_notification'), + ['class' => 'fitem_id_templatevars_editor'], $this->get_editor_options($context)); + $mform->addHelpButton('pulsenotification_footercontent_editor', 'footercontent', 'pulseaction_notification'); + $forminstance->pulse_email_placeholders($mform); + + // Preview Button. + $test = $mform->addElement('button', 'pulsenotification_preview', get_string('preview', 'pulseaction_notification')); + $mform->addHelpButton('pulsenotification_preview', 'preview', 'pulseaction_notification'); + + // Email tempalte placholders. + $PAGE->requires->js_call_amd('mod_pulse/module', 'init'); + $contextid = $PAGE->context->id; + $PAGE->requires->js_call_amd('pulseaction_notification/chaptersource', 'previewNotification', ['contextid' => $contextid]); + } + + /** + * Get list of options in 30 mins timeinterval for 24 hrs. + * + * @return array + */ + public function get_times() { + + $starttime = new DateTime('00:00'); // Set the start time to midnight. + $endtime = new DateTime('23:59'); // Set the end time to just before midnight of the next day. + + // Create an interval of 30 minutes. + $interval = new \DateInterval('PT30M'); // PT30M represents 30 minutes. + + // Create a DatePeriod to iterate through the day with the specified interval. + $timeperiod = new DatePeriod($starttime, $interval, $endtime); + + // Loop through the DatePeriod and add each timestamp to the array. + $timelist = array(); + foreach ($timeperiod as $time) { + $timelist[$time->format('H:i')] = $time->format('H:i'); // Format the timestamp as HH:MM. + } + + return $timelist; + } +} diff --git a/actions/notification/classes/external.php b/actions/notification/classes/external.php new file mode 100644 index 0000000..4f696e1 --- /dev/null +++ b/actions/notification/classes/external.php @@ -0,0 +1,87 @@ +. + +/** + * Notification pulse action external functions defined. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace pulseaction_notification; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_multiple_structure; +use core_external\external_single_structure; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/externallib.php'); + +/** + * Pulse Notification action external methods. + */ +class external extends \external_api { + + /** + * Get list of chapters for the book module function parameters. + * @return object type of the badge type. + */ + public static function get_chapters_parameters() { + return new \external_function_parameters( + array('mod' => new \external_value(PARAM_INT, 'Book module cmid ', VALUE_OPTIONAL)) + ); + } + + /** + * Get list of badges based on the requested type. + * + * @param string $mod ID of the course module. + * @return array $type List of badge types. + */ + public static function get_chapters($mod = null) { + global $CFG; + + if (isset($mod)) { + $cmid = $mod; + $chapters = \pulseaction_notification\notification::load_book_chapters($cmid); + foreach ($chapters as $chapterid => $chapter) { + $list[] = ['value' => $chapter->id, 'label' => $chapter->title]; + } + } + + return $list ?? []; + } + + /** + * Return chapters list data definition. + * + * @return array list of chapaters. + */ + public static function get_chapters_returns() { + return new \external_multiple_structure( + new \external_single_structure( + array( + 'value' => new \external_value(PARAM_INT, 'Chapter ID', VALUE_OPTIONAL), + 'label' => new \external_value(PARAM_TEXT, 'Chapter title', VALUE_OPTIONAL), + ) + ), '', VALUE_OPTIONAL + ); + } + +} diff --git a/actions/notification/classes/local/entities/notification.php b/actions/notification/classes/local/entities/notification.php new file mode 100644 index 0000000..dea7be1 --- /dev/null +++ b/actions/notification/classes/local/entities/notification.php @@ -0,0 +1,306 @@ +. + +/** + * Pulse notification entities for report builder. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace pulseaction_notification\local\entities; + +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\report\{column, filter}; +use core_reportbuilder\local\filters\{date, number, select, text}; +use core_reportbuilder\local\helpers\format; +use html_writer; +use pulseaction_notification\notification as pulsenotification; +use lang_string; + +/** + * Pulse notification entity base for report source. + */ +class notification extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + + return [ + 'user' => 'plnu', + 'context' => 'plnctx', + 'course' => 'plnc', + 'pulseaction_notification_sch' => 'plnsch', + 'pulse_autoinstances' => 'plni', + 'pulse_autotemplates' => 'plnt', + 'pulse_autotemplates_ins' => 'plnti', + 'pulseaction_notification_ins' => 'plani', + 'pulseaction_notification' => 'plan', + 'cohort_members' => 'chtm', + 'cohort' => 'cht' + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('notificationreport', 'pulseaction_notification'); + } + + /** + * Initialise the notification datasource columns and filter, conditions. + * + * @return base + */ + public function initialise(): base { + + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + list($filters, $conditions) = $this->get_all_filters(); + foreach ($filters as $filter) { + $this->add_filter($filter); + } + + foreach ($conditions as $condition) { + $this->add_condition($condition); + } + + return $this; + } + + /** + * List of columns available for this notfication datasource. + * + * @return array + */ + protected function get_all_columns(): array { + + $notificationschalias = $this->get_table_alias('pulseaction_notification_sch'); + $templatesalias = $this->get_table_alias('pulse_autotemplates'); + $templatesinsalias = $this->get_table_alias('pulse_autotemplates_ins'); + + $instancealias = $this->get_table_alias('pulse_autoinstances'); + $notificationalias = $this->get_table_alias('pulseaction_notification'); + + $notificationinsalias = $this->get_table_alias('pulseaction_notification_ins'); + + // Time the schedule is created. + $columns[] = (new column( + 'timecreated', + new lang_string('timecreated', 'pulseaction_notification'), + $this->get_entity_name() + )) + ->set_is_sortable(true) + ->add_field("{$notificationschalias}.timecreated") + ->add_callback(static function ($value, $row): string { + return userdate($value); + }); + + // Schedule time to send notification. + $columns[] = (new column( + 'scheduletime', + new lang_string('scheduledtime', 'pulseaction_notification'), + $this->get_entity_name() + )) + ->set_is_sortable(true) + ->add_field("{$notificationschalias}.scheduletime") + ->add_callback(static function ($value, $row): string { + return userdate($value); + }); + + // Message type field. + $columns[] = (new column( + 'messagetype', + new lang_string('messagetype', 'pulseaction_notification'), + $this->get_entity_name() + )) + ->set_is_sortable(true) + ->add_field("IF ({$templatesalias}.title <> '', {$templatesalias}.title, {$templatesinsalias}.title)", 'title') + ->add_callback(fn($val, $row) => format_string($val)); + + // Subject field. + $columns[] = (new column( + 'subject', + new lang_string('subject', 'pulseaction_notification'), + $this->get_entity_name() + )) + ->set_is_sortable(true) + ->add_field("IF ({$notificationinsalias}.subject <> '', + {$notificationinsalias}.subject, {$notificationalias}.subject)", "subject") + ->add_field("{$templatesinsalias}.instanceid") + ->add_field("{$notificationschalias}.userid") + ->add_callback([pulsenotification::class, 'get_schedule_subject']); + + // Status of the schedule. + $columns[] = (new column( + 'status', + new lang_string('status', 'pulseaction_notification'), + $this->get_entity_name() + )) + ->set_is_sortable(true) + ->add_field("{$notificationschalias}.status") + ->add_field("{$instancealias}.status", "instancestatus") + ->add_callback([pulsenotification::class, 'get_schedule_status']); + + return $columns; + } + + /** + * Defined filters for the notification entities. + * + * @return array + */ + protected function get_all_filters(): array { + global $DB; + + $notificationschalias = $this->get_table_alias('pulseaction_notification_sch'); + $templatesalias = $this->get_table_alias('pulse_autotemplates'); + $templatesinsalias = $this->get_table_alias('pulse_autotemplates_ins'); + + $instancealias = $this->get_table_alias('pulse_autoinstances'); + $notificationinsalias = $this->get_table_alias('pulseaction_notification_ins'); + + $cohortmembersalias = $this->get_table_alias('cohort_members'); + $cohortalias = $this->get_table_alias('cohort'); + + $useralias = $this->get_table_alias('user'); + + // Automation instance id. + $conditions[] = (new filter( + number::class, + 'instanceid', + new lang_string('instanceid', 'pulseaction_notification'), + $this->get_entity_name(), + "{$instancealias}.id" + )); + + // Automation instance. + $filters[] = (new filter( + text::class, + 'automationinstance', + new lang_string('messagetype', 'pulseaction_notification'), + $this->get_entity_name(), + "IF ({$templatesinsalias}.title <> '', {$templatesinsalias}.title, {$templatesalias}.title)" + )); + + // Automation template. + $filters[] = (new filter( + text::class, + 'automationtemplate', + new lang_string('messagetype', 'pulseaction_notification'), + $this->get_entity_name(), + "{$templatesalias}.title" + )); + + // Status of the schedule. + $filters[] = (new filter( + select::class, + 'status', + new lang_string('status', 'pulseaction_notification'), + $this->get_entity_name(), + "{$notificationschalias}.status", + ))->set_options([ + 1 => get_string('onhold', 'pulseaction_notification'), + 2 => get_string('queued', 'pulseaction_notification'), + 3 => get_string('sent', 'pulseaction_notification'), + 0 => get_string('failed', 'pulseaction_notification') + ]); + + // Scheduled time date filter. + $filters[] = (new filter( + date::class, + 'timecreated', + new lang_string('schedulecreatedtime', 'pulseaction_notification'), + $this->get_entity_name(), + "{$notificationschalias}.timecreated" + )); + + // Filter by the schedule nextrun time. + $filters[] = (new filter( + date::class, + 'scheduletime', + new lang_string('scheduledtime', 'pulseaction_notification'), + $this->get_entity_name(), + "{$notificationschalias}.scheduletime" + )); + + // Cohort based filters. + $options = $DB->get_records_menu('cohort', [], '', 'id, name'); + $filters[] = (new filter( + select::class, + 'cohort', + new lang_string('cohort', 'cohort'), + $this->get_entity_name(), + "{$cohortalias}.id" + ))->add_join(" + JOIN {user} {$useralias} ON {$useralias}.id = {$notificationschalias}.userid + JOIN {cohort_members} {$cohortmembersalias} on {$cohortmembersalias}.userid = {$useralias}.id + JOIN {cohort} {$cohortalias} on {$cohortalias}.id = {$cohortmembersalias}.cohortid + ")->set_options($options); + + // Conditions. + $options = $DB->get_records_menu('cohort', [], '', 'id, name'); + $conditions[] = (new filter( + select::class, + 'cohort', + new lang_string('cohort', 'cohort'), + $this->get_entity_name(), + "{$cohortalias}.id" + ))->add_join(" + JOIN {user} {$useralias} ON {$useralias}.id = {$notificationschalias}.userid + JOIN {cohort_members} {$cohortmembersalias} on {$cohortmembersalias}.userid = {$useralias}.id + JOIN {cohort} {$cohortalias} on {$cohortalias}.id = {$cohortmembersalias}.cohortid + ")->set_options($options); + + return [$filters, $conditions]; + } + + /** + * Schedule join sql. + * + * @return string + */ + public function schedulejoin() { + + $notificationschalias = $this->get_table_alias('pulseaction_notification_sch'); + + $autoinstancesalias = $this->get_table_alias('pulse_autoinstances'); + $autotemplatesalias = $this->get_table_alias('pulse_autotemplates'); + $autotemplatesinsalias = $this->get_table_alias('pulse_autotemplates_ins'); + $notificationinsalias = $this->get_table_alias('pulseaction_notification_ins'); + $notificationalias = $this->get_table_alias('pulseaction_notification'); + + return " + JOIN {pulse_autoinstances} AS {$autoinstancesalias} ON {$autoinstancesalias}.id = {$notificationschalias}.instanceid + JOIN {pulse_autotemplates} AS {$autotemplatesalias} ON {$autotemplatesalias}.id = {$autoinstancesalias}.templateid + JOIN {pulse_autotemplates_ins} AS {$autotemplatesinsalias} + ON {$autotemplatesinsalias}.instanceid = {$autoinstancesalias}.id + JOIN {pulseaction_notification_ins} AS {$notificationinsalias} + ON {$notificationinsalias}.instanceid = {$notificationschalias}.instanceid + JOIN {pulseaction_notification} AS {$notificationalias} + ON {$notificationalias}.templateid = {$autoinstancesalias}.templateid"; + } +} diff --git a/actions/notification/classes/notification.php b/actions/notification/classes/notification.php new file mode 100644 index 0000000..ac5ae5a --- /dev/null +++ b/actions/notification/classes/notification.php @@ -0,0 +1,988 @@ +. + +/** + * Notification pulse action - Create and Manage notifications. + * + * This Controller create a schedule for users, verify their availability based on conditions. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification; + +use book; +use DateTime; +use mod_pulse\automation\helper; +use mod_pulse\automation\instances; +use mod_pulse\helper as pulsehelper; +use mod_pulse\plugininfo\pulseaction; +use moodle_url; +use html_writer; +use stdClass; +use tool_dataprivacy\form\context_instance; + +/** + * Notification helper, Manage user schedules CRUD. + */ +class notification { + + /** + * Represents a course teacher as type of notification sender. + * @var int + */ + const SENDERCOURSETEACHER = 1; + + /** + * Represents a group teacher as type of notification sender. + * @var int + */ + const SENDERGROUPTEACHER = 2; + + /** + * Represents a tenent role as type of notification sender. + * @var int + */ + const SENDERTENANTROLE = 3; + + /** + * Represents a custom email as type of notification sender. + * @var int + */ + const SENDERCUSTOM = 4; + + /** + * Represents a notification interval is once. + * @var int + */ + const INTERVALONCE = 1; // Once. + + /** + * Represents a notification interval is Daily. + * @var int + */ + const INTERVALDAILY = 2; // Daily. + + /** + * Represents a notification interval is weekly. + * @var int + */ + const INTERVALWEEKLY = 3; // Weekly. + + /** + * Represents a notification interval is monthly. + * @var int + */ + const INTERVALMONTHLY = 4; // Montly. + + /** + * Represents there is no delay to send the notification. + * @var int + */ + const DELAYNONE = 0; + + /** + * Represents a delay before send the notifications. + * @var int + */ + const DELAYBEFORE = 1; + + /** + * Represents a delay after some time to send the notifications. + * @var int + */ + const DELAYAFTER = 2; + + /** + * Represents a length of the dynamic content is teaser. + * @var int + */ + const LENGTH_TEASER = 1; + + /** + * Represents the dynamic content included the link to access the desired module. + * @var int + */ + const LENGTH_LINKED = 2; + + /** + * Represents there is not links included with dynamic content. + * @var int + */ + const LENGTH_NOTLINKED = 3; + + /** + * Represents the description of the dynamic module is included in the notification. + * @var int + */ + const DYNAMIC_DESCRIPTION = 1; + + /** + * Represents the content of the dynamic module is included in the notification. + * @var int + */ + const DYNAMIC_CONTENT = 2; + + /** + * Represents the notification schedule status is failed. + * @var int + */ + const STATUS_FAILED = 0; + + /** + * Represents the notification schedule status is disabled. + * @var int + */ + const STATUS_DISABLED = 1; + + /** + * Represents the notification schedule status is queued. + * @var int + */ + const STATUS_QUEUED = 2; + + /** + * Represents the notification schedule status is sent. + * @var int + */ + const STATUS_SENT = 3; + + /** + * Represents the user completed the suppress module. + * @var int + */ + const SUPPRESSREACHED = 1; + + /** + * The record of the notification instance with templates and general conditions. + * + * @var stdclass + */ + protected $instancedata; + + /** + * The merged notification data based on instance overrides. + * + * @var stdclass + */ + protected $notificationdata; + + /** + * The ID of the action notification table. + * @var int + */ + protected $notificationid; // Notification table id. + + /** + * Create the instance of the notification controller. + * + * @param int $notificationid Notification instance record id NOT autoinstanceid. + * @return notification + */ + public static function instance($notificationid) { + static $instance; + + if (!$instance || ($instance && $instance->notificationid != $notificationid)) { + $instance = new self($notificationid); + } + + return $instance; + } + + /** + * Contructor for this notification controller. + * + * @param int $notificationid Notification table id. + */ + protected function __construct(int $notificationid) { + $this->notificationid = $notificationid; + } + + /** + * Create the notification instance and set the data to this class. + * + * @return void + */ + protected function create_instance_data() { + global $DB; + + $notification = $DB->get_record('pulseaction_notification_ins', ['id' => $this->notificationid]); + + $instance = instances::create($notification->instanceid); + $autoinstance = $instance->get_instance_data(); + + $notificationdata = $autoinstance->actions['notification']; + + unset($autoinstance->actions['notification']); // Remove actions. + + $this->set_notification_data($notificationdata, $autoinstance); + } + + /** + * Set the notification data to global. Decode and do other structure updates for the data before setup. + * + * @param stdclass $notificationdata Contains notification data. + * @param stdclass $instancedata Contains other than actions. + * @return void + */ + public function set_notification_data($notificationdata, $instancedata) { + $data = (object) $notificationdata; + $this->notificationdata = $this->update_data_structure($data); + + $data = (object) $instancedata; + $this->instancedata = $instancedata; + } + + /** + * Decode the encoded json values to array, to further uses. + * + * @param stdclass $actiondata + * @return stdclass $actiondata Updated action data. + */ + public function update_data_structure($actiondata) { + + $actiondata->recipients = is_array($actiondata->recipients) + ? $actiondata->recipients : json_decode($actiondata->recipients); + + $actiondata->bcc = is_array($actiondata->bcc) ? $actiondata->bcc : json_decode($actiondata->bcc); + $actiondata->cc = is_array($actiondata->cc) ? $actiondata->cc : json_decode($actiondata->cc); + + $actiondata->notifyinterval = is_array($actiondata->notifyinterval) + ? $actiondata->notifyinterval : json_decode($actiondata->notifyinterval, true); + + return $actiondata; + } + + /** + * Generate the data set for the user to create schedule for this instance. + * + * @param int $userid ID of the user to create schedule. + * @return array $record Record to insert into schdeule. + */ + protected function generate_schedule_record(int $userid) { + + $record = [ + 'instanceid' => $this->notificationdata->instanceid, + 'userid' => $userid, + 'type' => $this->notificationdata->notifyinterval['interval'], + 'status' => self::STATUS_QUEUED, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + return $record; + } + + /** + * Insert the schedule to database, verify if the schedule is already in queue then override the schedule with given record. + * + * @param stdclass $data + * @param bool $newschedule + * @return int Inserted schedule ID. + */ + protected function insert_schedule($data, $newschedule=false) { + global $DB; + + $sql = 'SELECT * FROM {pulseaction_notification_sch} + WHERE instanceid = :instanceid AND userid = :userid AND (status = :disabledstatus OR status = :queued)'; + + if ($record = $DB->get_record_sql($sql, [ + 'instanceid' => $data['instanceid'], 'userid' => $data['userid'], 'disabledstatus' => self::STATUS_DISABLED, + 'queued' => self::STATUS_QUEUED + ])) { + + $data['id'] = $record->id; + // Update the status to enable for notify. + $DB->update_record('pulseaction_notification_sch', $data); + + return $record->id; + } + + // Dont create new schedule for already notified users until is not new schedule. + // It prevents creating new record for user during the update of instance interval. + if (!$newschedule && $DB->record_exists('pulseaction_notification_sch', [ + 'instanceid' => $data['instanceid'], 'userid' => $data['userid'], 'status' => self::STATUS_SENT + ])) { + return false; + } + + return $DB->insert_record('pulseaction_notification_sch', $data); + } + + /** + * Disable the queued schdule of the given user. + * + * @param int $userid + * @return void + */ + protected function disable_user_schedule($userid) { + global $DB; + + $sql = "SELECT * FROM {pulseaction_notification_sch} + WHERE instanceid = :instanceid AND userid = :userid AND (status = :disabledstatus OR status = :queued)"; + + $params = [ + 'instanceid' => $this->notificationdata->instanceid, 'userid' => $userid, 'disabledstatus' => self::STATUS_DISABLED, + 'queued' => self::STATUS_QUEUED + ]; + + if ($record = $DB->get_record_sql($sql, $params)) { + $DB->set_field('pulseaction_notification_sch', 'status', self::STATUS_DISABLED, ['id' => $record->id]); + } + } + + /** + * Remove the queued and disabled schedules of this user. + * + * @param int $userid + * @return void + */ + public function remove_user_schedules($userid) { + global $DB; + + $sql = "SELECT * FROM {pulseaction_notification_sch} + WHERE instanceid = :instanceid AND userid = :userid AND (status = :disabledstatus OR status = :queued)"; + + $params = [ + 'instanceid' => $this->notificationdata->instanceid, 'userid' => $userid, 'disabledstatus' => self::STATUS_DISABLED, + 'queued' => self::STATUS_QUEUED + ]; + + if ($record = $DB->get_record_sql($sql, $params)) { + $DB->delete_records('pulseaction_notification_sch', ['id' => $record->id]); + } + } + + /** + * Get the current schedule created for the user related to specific instance. + * + * @param stdclass $data Data with instance id and user id. + * @return stdclass|null Record of the current schedule. + */ + protected function get_schedule($data) { + global $DB; + + if ($record = $DB->get_record('pulseaction_notification_sch', [ + 'instanceid' => $data->instanceid, 'userid' => $data->userid + ])) { + return $record; + } + + return false; + } + + /** + * Find the sent time of the last schedule to the user for the specific instance. + * + * @param int $userid + * @return int|null Time of the last schedule notified to the user for the specific instance + */ + protected function find_last_notifiedtime($userid) { + global $DB; + + $id = $this->notificationdata->instanceid; + + // Get last notified schedule for this instance to the user. + $condition = array('instanceid' => $id, 'userid' => $userid, 'status' => self::STATUS_SENT); + $records = $DB->get_records('pulseaction_notification_sch', $condition, 'id DESC', '*', 0, 1); + + return !empty($records) ? current($records)->notifiedtime : ''; + } + + /** + * Find a count of the schedules sent to the user for the current notification instance. + * + * @param int $userid ID of the user to fetch the counts + * @return int|null Count of the schedules sent to the user + */ + protected function find_notify_count($userid) { + global $DB; + + $id = $this->notificationdata->instanceid; + + // Get last notified schedule for this instance to the user. + $condition = array('instanceid' => $id, 'userid' => $userid, 'status' => self::STATUS_SENT); + $records = $DB->get_records('pulseaction_notification_sch', $condition, 'id DESC', '*', 0, 1); + + return !empty($records) ? current($records)->notifycount : ''; + } + + /** + * Verify the user is already notified for this instance. It verify the lastrun is empty for the user record. + * + * Note: Use this method to verify the instance with interval once. + * + * @param int $userid + * @return bool + */ + protected function is_user_notified(int $userid) { + global $DB; + + $id = $this->notificationdata->instanceid; + $condition = ['instanceid' => $id, 'userid' => $userid, 'status' => self::STATUS_SENT]; + if ($record = $DB->get_record('pulseaction_notification_sch', $condition)) { + return $record->notifiedtime != null ? true : false; + } + return false; + } + + /** + * Remove the schdeduled notifications for this instance. + * + * @param int $status + * @return void + */ + protected function remove_schedules($status=self::STATUS_SENT) { + global $DB; + + $DB->delete_records('pulseaction_notification_sch', ['instanceid' => $this->instancedata->id, 'status' => $status]); + } + + /** + * Removes the current queued schedules and recreate the schedule for all the qualified users. + * + * @return void + */ + public function recreate_schedule_forinstance() { + // Remove the current queued schedules. + $this->remove_schedules(self::STATUS_QUEUED); + // Create the schedules for all users. + $this->create_schedule_forinstance(); + } + + /** + * Create schdule for the instance. + * + * @param bool $newenrolment Is the schedule for new enrolments. + * @return void + */ + public function create_schedule_forinstance($newenrolment=false) { + // Generate the notification instance data. + if (empty($this->instancedata)) { + $this->create_instance_data(); + } + + // Course context. + $context = \context_course::instance($this->instancedata->courseid); + // Roles to receive the notifications. + $roles = $this->notificationdata->recipients; + if (empty($roles)) { + // No roles are defined to recieve notifications. Remove the schedules for this instance. + $this->remove_schedules(); + return true; // No roles are defined to recieve notifications. Break the schedule creation. + } + // Get the users for this receipents roles. + $users = $this->get_users_withroles($roles, $context); + foreach ($users as $userid => $user) { + $this->create_schedule_foruser($user->id, null, 0, null, $newenrolment); + } + + return true; + } + + /** + * Create schedule for the user. + * + * @param int $userid + * @param string $lastrun + * @param integer $notifycount + * @param int $expectedruntime Timestamp of the time to run. + * @param bool $isnewuser + * @param bool $newschedule + * @return int ID of the created schedule. + */ + public function create_schedule_foruser($userid, $lastrun='', $notifycount=0, $expectedruntime=null, + $isnewuser=false, $newschedule=false) { + + if (empty($this->instancedata)) { + $this->create_instance_data(); + } + + // Verify the user passed the instance condition. + if (!instances::create($this->notificationdata->instanceid) + ->find_user_completion_conditions($this->instancedata->condition, $this->instancedata, $userid, $isnewuser)) { + // Remove the user condition. + $this->disable_user_schedule($userid); + return true; + } + + // TODO: Verify it realy need to verify the suppress reached status. + + $notifycount = $notifycount ?: $this->find_notify_count($userid); + // Verify the Limit is reached, if 0 then its unlimited. + if ($this->notificationdata->notifylimit > 0 && ($notifycount >= $this->notificationdata->notifylimit)) { + return false; + } + + // Notification interval is once per user, it already notified to the user. break the trigger here. + if ($this->notificationdata->notifyinterval['interval'] == self::INTERVALONCE && $this->is_user_notified($userid)) { + return true; + } + + $lastrun = $lastrun ?: $this->find_last_notifiedtime($userid); + + // Generate the schedule record. + $data = $this->generate_schedule_record($userid); + + $data['notifycount'] = $notifycount; + // ...# Find the next run. + $nextrun = $this->generate_the_scheduletime($userid, $lastrun, $expectedruntime); + // Include the next run to schedule. + $data['scheduletime'] = $nextrun; + // Insert the new schedule or update schedule. + $scheduleid = $this->insert_schedule($data, $newschedule); + + return $scheduleid; + } + + /** + * Generate the schedule time for this notification. + * + * @param int $userid + * @param int $lastrun + * @param int $expectedruntime + * @return int + */ + protected function generate_the_scheduletime($userid, $lastrun=null, $expectedruntime=null) { + global $DB; + + $data = $this->notificationdata; + $data->userid = $userid; + + $now = new DateTime('now', \core_date::get_server_timezone_object()); + + if ($expectedruntime) { + $expectedruntime = $now->setTimestamp($expectedruntime); + } + + $nextrun = $expectedruntime ?: $now; + if (!empty($lastrun)) { + $lastrun = ($lastrun instanceof DateTime) + ?: (new DateTime('now', \core_date::get_server_timezone_object()))->setTimestamp($lastrun); + $nextrun = $lastrun; + } + + $interval = $data->notifyinterval['interval']; + + switch ($interval) { + + case self::INTERVALDAILY: + $time = $data->notifyinterval['time']; + $nextrun->modify('+1 day'); // TODO: Change this to Dateinterval(). + $timeex = explode(':', $time); + $nextrun->setTime(...$timeex); + break; + + case self::INTERVALWEEKLY: + $day = $data->notifyinterval['weekday']; + $time = $data->notifyinterval['time']; + $nextrun->modify("Next ".$day); + $timeex = explode(':', $time); + $nextrun->setTime(...$timeex); + break; + + case self::INTERVALMONTHLY: + $monthdate = $data->notifyinterval['monthdate']; + if ($monthdate != 31) { // If the date is set as 31 then use the month end. + $nextrun->modify('first day of next month'); + $date = $data->notifyinterval['monthdate'] + ? $data->notifyinterval['monthdate'] - 1 : $data->notifyinterval['monthdate']; + $nextrun->modify("+$date day"); + } else { + $nextrun->modify('last day of next month'); + } + + $time = $data->notifyinterval['time'] ?? '0:00'; + $timeex = explode(':', $time); + $nextrun->setTime(...$timeex); + break; + + case self::INTERVALONCE: + $nextrun = $expectedruntime ?: $now; + break; + } + + // Add limit of available. + if ($data->notifydelay == self::DELAYAFTER) { + $delay = $data->delayduration; + $nextrun->modify("+ $delay seconds"); + } else if ($data->notifydelay == self::DELAYBEFORE) { + $delay = $data->delayduration; + + if ($expectedruntime) { + // SEssion condition only send the expected runtime. + // Reduce the delay directly from the expected runtime. + $nextrun->modify("- $delay seconds"); + + } else if (method_exists('\pulsecondition_session\conditionform', 'get_session_time')) { + // Confirm any f2f module added in condition. + $sessionstarttime = \pulsecondition_session\conditionform::get_session_time($data, $this->instancedata); + + if (!empty($sessionstarttime)) { + $nextrun->setTimestamp($sessionstarttime); + $nextrun->modify("- $delay seconds"); + } + } + } + + return $nextrun->getTimestamp(); + } + + /** + * Get the users assigned in the roles. + * + * @param array $roles Role ids to fetch + * @param \context $context + * @return array List of the users. + */ + protected function get_users_withroles(array $roles, $context) { + global $DB; + + // TODO: Cache the role users. + if (empty($roles)) { + return []; + } + + list($insql, $inparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'rle'); + + // TODO: Define user fields, never get entire fields. + $rolesql = "SELECT DISTINCT u.id, u.*, ra.roleid FROM {role_assignments} ra + JOIN {user} u ON u.id = ra.userid + JOIN {role} r ON ra.roleid = r.id + LEFT JOIN {role_names} rn ON (rn.contextid = :ctxid AND rn.roleid = r.id) + WHERE (ra.contextid = :ctxid2 ) AND ra.roleid $insql ORDER BY u.id"; + + $params = array('ctxid' => $context->id, 'ctxid2' => $context->id) + $inparams; + + $users = $DB->get_records_sql($rolesql, $params); + + return $users; + } + + /** + * Build the notification content. + * + * @param stdClass|null $cm Course module + * @param \context $context + * @param array $overrides + * + * @return string Notification content. + */ + public function build_notification_content(?stdClass $cm=null, $context=null, $overrides=[]) { + global $CFG, $DB; + + $syscontext = \context_system::instance(); + + $headercontent = $this->notificationdata->headercontent; + $staticcontent = $this->notificationdata->staticcontent; + $footercontent = $this->notificationdata->footercontent; + + // Rewrite the plugin url for files in editors. + foreach (['headercontent', 'staticcontent', 'footercontent'] as $editor) { + + $content = $$editor; + + $field = 'pulsenotification_'.$editor; + $prefix = (isset($overrides[$editor]) || isset($overrides[$field]) || isset($overrides[$field.'_editor'])) + ? '_instance' : ''; + $id = (isset($overrides[$editor]) || isset($overrides[$field]) || isset($overrides[$field.'_editor'])) + ? $this->notificationdata->instanceid : $this->instancedata->templateid; + + $$editor = file_rewrite_pluginfile_urls( + $content, 'pluginfile.php', $syscontext->id, + 'mod_pulse', $field.$prefix, $id + ); + } + + // Include the dynamic contents. + $dynamiccontent = $this->notificationdata->dynamiccontent; + if ($dynamiccontent) { + if ($cm == null) { + $module = get_coursemodule_from_id('', $dynamiccontent); + $cm = (object) [ + 'instance' => $module->instance, + 'modname' => $module->modname, + 'id' => $module->id, + ]; + } + + $modcontext = \context_module::instance($dynamiccontent); + + $staticcontent .= self::generate_dynamic_content( + $this->notificationdata->contenttype, + $this->notificationdata->contentlength, + $this->notificationdata->chapterid ?? 0, + $modcontext, + $cm + ); // Concat the dynamic content after static content. + } + + $finalcontent = $headercontent . $staticcontent . $footercontent; + return format_text($finalcontent, FORMAT_HTML, ['noclean' => true, 'overflowdiv' => true]); + } + + /** + * Gernerate the content based on dynamic module to attach with the notification content. + * + * @param string $contenttype + * @param int $contentlength + * @param int $chapterid + * @param \context $context + * @param stdclass $cm + * + * @return void + */ + public static function generate_dynamic_content($contenttype, $contentlength, $chapterid, $context, $cm) { + + global $CFG, $DB; + + require_once($CFG->dirroot.'/lib/modinfolib.php'); + require_once($CFG->dirroot.'/mod/book/lib.php'); + require_once($CFG->libdir.'/filelib.php'); + + if ($contenttype == self::DYNAMIC_CONTENT) { + + if ($cm->modname == 'book') { + $chapter = $DB->get_record('book_chapters', ['id' => $chapterid, 'bookid' => $cm->instance]); + $chaptertext = \file_rewrite_pluginfile_urls( + $chapter->content, 'pluginfile.php', $context->id, 'mod_book', 'chapter', $chapter->id); + + $content = format_text($chaptertext, $chapter->contentformat, ['noclean' => true, 'overflowdiv' => true]); + $link = new moodle_url('/mod/book/view.php', ['id' => $cm->id, 'chapterid' => $chapterid]); + } else { + $page = $DB->get_record('page', array('id' => $cm->instance), '*', MUST_EXIST); + + $content = file_rewrite_pluginfile_urls( + $page->content, 'pluginfile.php', $context->id, 'mod_page', 'content', $page->revision); + $link = new moodle_url('/mod/page/view.php', ['id' => $cm->id]); + } + + } else { + // TODO: Need to cache module intro content. + $activity = $DB->get_record("$cm->modname", ['id' => $cm->instance]); + $content = format_module_intro($cm->modname, $activity, $cm->id, true); + $link = new moodle_url("/mod/$cm->modname/view.php", ['id' => $cm->id]); + } + + // Verify the contnet length. + switch ($contentlength) { + + case self::LENGTH_TEASER: + preg_match('/

(.*?)<\/p>/s', $content, $match); + $content = $match[0] ?? $content; + + $content .= html_writer::link($link, get_string('readmore', 'pulseaction_notification')); + break; + + case self::LENGTH_LINKED: + $content .= html_writer::link($link, get_string('readmore', 'pulseaction_notification')); + break; + } + + return $content; + } + + /** + * Generate the details of the notification to send. + * + * @param stdclass $moddata + * @param stdclass $user + * @param \context $context + * @param array $notificationoverrides + * @return array Basic details to send notification. + */ + public function generate_notification_details($moddata, $user, $context, $notificationoverrides=[]) { + + // Find the cc and bcc users for this schedule. + $roles = array_merge($this->notificationdata->cc, $this->notificationdata->bcc); + // Get the users for this bcc and cc roles. + $roleusers = $this->get_users_withroles($roles, $context); + + // Filter the cc users for this instance. + $cc = $this->notificationdata->cc; + $ccusers = array_filter($roleusers, function($value) use ($cc) { + return in_array($value->roleid, $cc); + }); + + // Filter the bcc users for this instance. + $bcc = $this->notificationdata->bcc; + $bccusers = array_filter($roleusers, function($value) use ($bcc) { + return in_array($value->roleid, $bcc); + }); + + $result = [ + 'recepient' => (object) $user, + 'cc' => implode(',', array_column($ccusers, 'email')), + 'bcc' => implode(',', array_column($bccusers, 'email')), + 'subject' => format_string($this->notificationdata->subject), + 'content' => $this->build_notification_content($moddata, $context, $notificationoverrides), + ]; + + return (object) $result; + } + + /** + * Get the tenant role sender user. + * + * @param stdclass $scheduledata + * @return stdclass + */ + public function get_tenantrole_sender($scheduledata) { + // TODO: Tenant based sender fetch goes here. + return (object) []; + } + + /** + * Find the sender users for the course context, fetched the teachers based on group assignment. + * + * @param \context $coursecontext + * @param int $groupid + * @return stdclass + */ + protected static function get_sender_users($coursecontext, $groupid) { + + $withcapability = 'pulseaction/notification:sender'; + $sender = get_enrolled_users( + $coursecontext, + $withcapability, + $groupid, + 'u.*', + null, + 0, + 1, + true + ); + + return !empty($sender) ? current($sender) : []; + } + + /** + * Load book chapters. + * + * @param int $cmid Course module id. + * @return array + */ + public static function load_book_chapters(int $cmid) { + global $DB; + $mod = get_coursemodule_from_id('', $cmid); + + $list = ''; + if ($mod->modname == 'book') { + $chapters = $DB->get_records('book_chapters', ['bookid' => $mod->instance]); + return $chapters; + } + return $list; + } + + /** + * Get the status of the schedule. + * + * @param int $value + * @param stdclass $row + * @return string + */ + public static function get_schedule_status($value, $row) { + + if ($value == self::STATUS_DISABLED) { + return get_string('onhold', 'pulseaction_notification'); + } else if ($value == self::STATUS_QUEUED) { + if (!$row->instancestatus) { + return get_string('onhold', 'pulseaction_notification'); + } + return get_string('queued', 'pulseaction_notification'); + } else if ($value == self::STATUS_SENT) { + return get_string('sent', 'pulseaction_notification'); + } else { + return get_string('failed', 'pulseaction_notification'); + } + } + + /** + * Get the schedule subject to display in the reports. + * + * @param string $value Subject + * @param stdclass $row + * @return string + */ + public static function get_schedule_subject($value, $row) { + global $DB; + + $sender = \core_user::get_support_user(); + $courseid = $DB->get_field('pulse_autoinstances', 'courseid', ['id' => $row->instanceid]); + $user = (object) \core_user::get_user($row->userid); + $course = get_course($courseid ?? SITEID); + + list($subject, $messagehtml) = \mod_pulse\helper::update_emailvars('', $value, $course, $user, null, $sender); + + return $subject . html_writer::link('javascript:void(0);', '', [ + 'class' => 'pulse-automation-info-block', + 'data-target' => 'view-content', + 'data-instanceid' => $row->instanceid, + 'data-userid' => $row->userid + ]); + } + + /** + * Get the list of modules data for the placholders. + * + * @param array $modules List of modules. + * @return array + */ + public static function get_modules_data($modules) { + global $DB, $CFG; + + if (file_exists($CFG->dirroot.'/local/metadata/lib.php')) { + require_once($CFG->dirroot.'/local/metadata/lib.php'); + } + + $list = []; + foreach ($modules as $modname => $instances) { + + $tablename = $DB->get_prefix().$modname; + list($insql, $inparams) = $DB->get_in_or_equal($instances, SQL_PARAMS_NAMED, 'md'); + + $sql = "SELECT md.*, cm.id as cmid FROM $tablename md + JOIN {modules} m ON m.name = '$modname' + JOIN {course_modules} cm ON cm.instance = md.id AND cm.module = m.id + WHERE md.id $insql"; + + $records = $DB->get_records_sql($sql, $inparams); + + foreach ($records as $modid => $mod) { + $mod->type = $modname; + + if (isset($list[$modname][$modid])) { + continue; + } + + if (function_exists('local_metadata_load_data')) { + $newmod = (object) ['id' => $mod->cmid]; + local_metadata_load_data($newmod, CONTEXT_MODULE); + unset($newmod->cmid); + foreach ($newmod as $name => $value) { + $key = str_replace('local_metadata_field_', 'metadata', $name); + $mod->$key = $value; + } + } + + $list[$modname][$modid] = $mod; + } + } + + return $list; + } +} diff --git a/actions/notification/classes/reportbuilder/datasource/notification.php b/actions/notification/classes/reportbuilder/datasource/notification.php new file mode 100644 index 0000000..7a2a090 --- /dev/null +++ b/actions/notification/classes/reportbuilder/datasource/notification.php @@ -0,0 +1,136 @@ +. + +/** + * Pulse notification datasource for the schedules. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification\reportbuilder\datasource; + +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\course; +use core_reportbuilder\local\entities\user; + +/** + * Notification datasource definition for the list of schedules. + */ +class notification extends datasource { + + /** + * Return user friendly name of the datasource + * + * @return string + */ + public static function get_name(): string { + return get_string('formtab', 'pulseaction_notification'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + global $PAGE; + + $notificationentity = new \pulseaction_notification\local\entities\notification(); + $notificationalias = $notificationentity->get_table_alias('pulse_autoinstances'); + + $notificationschalias = $notificationentity->get_table_alias('pulseaction_notification_sch'); + $this->set_main_table('pulseaction_notification_sch', $notificationschalias); + $this->add_entity($notificationentity); + + // Force the join to be added so that course fields can be added first. + $this->add_join($notificationentity->schedulejoin()); + + // Add core user join. + $userentity = new user(); + $useralias = $userentity->get_table_alias('user'); + $userjoin = "JOIN {user} {$useralias} ON {$useralias}.id = {$notificationschalias}.userid"; + $this->add_entity($userentity->add_join($userjoin)); + + $coursentity = new course(); + $coursealias = $coursentity->get_table_alias('course'); + $coursejoin = "JOIN {course} {$coursealias} ON {$coursealias}.id = {$notificationalias}.courseid"; + $this->add_entity($coursentity->add_join($coursejoin)); + + if ($instance = optional_param('instanceid', null, PARAM_INT)) { + $this->add_base_condition_simple("{$notificationschalias}.instanceid", $instance); + } + + // Support for 4.2. + if (method_exists($this, 'add_all_from_entities')) { + $this->add_all_from_entities(); + } else { + // Add all the entities used in notification datasource. moodle 4.0 support. + $this->add_columns_from_entity($notificationentity->get_entity_name()); + $this->add_filters_from_entity($notificationentity->get_entity_name()); + $this->add_conditions_from_entity($notificationentity->get_entity_name()); + + $this->add_columns_from_entity($userentity->get_entity_name()); + $this->add_filters_from_entity($userentity->get_entity_name()); + $this->add_conditions_from_entity($userentity->get_entity_name()); + + $this->add_columns_from_entity($coursentity->get_entity_name()); + $this->add_filters_from_entity($coursentity->get_entity_name()); + $this->add_conditions_from_entity($coursentity->get_entity_name()); + } + + // Init the script to show the notification content in the modal. + $params = ['contextid' => \context_system::instance()->id]; + $PAGE->requires->js_call_amd('pulseaction_notification/chaptersource', 'reportModal', $params); + } + + /** + * Return the columns that will be added to the report once is created + * + * @return string[] + */ + public function get_default_columns(): array { + + return [ + 'course:fullname', + 'notification:messagetype', + 'notification:subject', + 'user:fullname', + 'notification:timecreated', + 'notification:scheduletime', + 'notification:status' + ]; + } + + /** + * Return the filters that will be added to the report once is created + * + * @return array + */ + public function get_default_filters(): array { + return []; + } + + /** + * Return the conditions that will be added to the report once is created + * + * @return array + */ + public function get_default_conditions(): array { + return [ + 'notification:instanceid' + ]; + } +} diff --git a/actions/notification/classes/schedule.php b/actions/notification/classes/schedule.php new file mode 100644 index 0000000..f980314 --- /dev/null +++ b/actions/notification/classes/schedule.php @@ -0,0 +1,540 @@ +. + +/** + * Notification pulse action - Send the scheduled notifications. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification; + +use mod_pulse\automation\helper; +use mod_pulse\automation\instances; +use mod_pulse\helper as pulsehelper; +use stdClass; + +/** + * Notification pulse action - Send the scheduled notifications. + */ +class schedule { + + /** + * The user object of support user. + * @var stdclass + */ + protected $supportuser; + + /** + * The automation instance data. + * @var stdclass + */ + protected $instancedata; + + /** + * The record of notification schedule. + * @var stdclass + */ + protected $schedulerecord; + + /** + * The record of entire notifcation schedule row. + * @var stdclass + */ + protected $schedule; + + /** + * The course object. + * @var stdclass + */ + protected $course; + + /** + * The user data object. + * @var stdclass + */ + protected $user; + + /** + * Notification record. + * + * @var notification + */ + protected $notification; + + /** + * List of overrides in the notification action. + * + * @var array + */ + protected $notificationoverrides; + + /** + * Notification data processed with overrides. + * + * @var stdclass + */ + protected $notificationdata; + + /** + * Course context. + * + * @var \context_course + */ + protected $coursecontext; // Context_module. + + /** + * List of conditions enabled in this instance. + * + * @var array + */ + protected $conditions; + + /** + * Schedule constructor. + */ + public function __construct() { + // Support user. + $this->supportuser = \core_user::get_support_user(); + + } + + /** + * Create a instance of this schedule controller. + * + * @return self + */ + public static function instance() { + + $self = new self(); + return $self; + } + + /** + * Sends scheduled notifications to users. + * + * @param int|null $userid (Optional) The ID of the user. If provided, notifications will be sent only to this user. + * + * @return void + */ + public function send_scheduled_notification($userid=null) { + global $DB, $CFG; + + require_once($CFG->dirroot.'/mod/pulse/automation/automationlib.php'); + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/lib/moodlelib.php'); + + // Genreate the SQL to fetch the list of schedules to send notification. + // It fetch the list based on the limit. + $schedules = $this->get_scheduled_records($userid); + // Get the modules list for the dynamiccontent will be used in this schedule. + // Reduce the query to fetch same modules again. + $modules = $this->get_modules_dynamic_content($schedules); + + foreach ($schedules as $sid => $schedule) { + + // Separete the values from the record and create each as class level variable object. + $this->build_schedule_values($schedule); + + // Verify the notification instance limit of notification is reached for this user. + if (isset($schedule->notifiycount) && $schedule->notifiycount >= $this->notificationdata->notifylimit) { + continue; // Limit reached break this continue to next user. + } + + $cmdata = (object) [ + 'modname' => $schedule->md_name, // Module name Book or page. + 'instance' => $schedule->cm_instance, + 'id' => $schedule->cm_id + ]; + // Generate the details to send the notification, it contains user, cc, bcc and schedule data. + $detail = $this->notification->generate_notification_details( + $cmdata, $this->user, $this->coursecontext, $this->notificationoverrides); + + $sender = $this->find_sender_user(); + + if (is_string($sender)) { + $replyto = $sender; + $sender = (object) [ + 'from' => $replyto, + 'firstname' => '', + 'lastname' => '', + 'firstnamephonetic' => '', + 'lastnamephonetic' => '', + 'middlename' => '', + 'alternatename' => '', + 'firstname' => '', + 'lastname' => '', + ]; + } + // Add bcc and CC to sender user custom headers. + $sender->customheaders = [ + "Bcc: $detail->bcc\r\n", + "Cc: $detail->cc\r\n", + ]; + + // Prepare the module data. based on dynamic content and includ the session data. + $mod = $this->prepare_moduledata_placeholders($modules, $cmdata); + + // Check the session condition are set for this notification. if its added then load the session data for placeholders. + $sessionin = in_array('session', (array) $this->instancedata->template->triggerconditions); + $sessionin = ($this->schedulerecord->con_isoverridden == 1) ? $this->schedulerecord->con_status : $sessionin; + if ($sessionin) { + $sessionconditiondata = (object) ['modules' => json_decode($this->schedulerecord->con_additional)->modules]; + $this->include_session_data($mod, $sessionconditiondata, $this->user->id); + } + + // Update the email placeholders. + list($subject, $messagehtml) = pulsehelper::update_emailvars( + $detail->content, $detail->subject, $this->course, $this->user, $mod, $sender); + + // Plain message. + $messageplain = html_to_text($messagehtml); + + // Courseid is needed in the message api. + $pulse = (object) ['course' => $this->course->id]; + // TODO: NOTE using notification API takes 16 queries. Direct email_to_user method will take totally 9 queries. + // Send the notification to user. + $messagesend = email_to_user($detail->recepient, $sender, $subject, + $messageplain, $messagehtml, '', '', true, $replyto ?? ''); + + if ($messagesend) { + // Update the current time as lastrun. + // Update the lastrun and increase the limit. + $notifiedtime = time(); + + $notifycount = $this->schedule->notifycount ? $this->schedule->notifycount + 1 : 1; + $update = [ + 'id' => $this->schedule->id, + 'notifycount' => $notifycount, + 'status' => notification::STATUS_SENT, + 'notifiedtime' => $notifiedtime, + ]; + + // Generate a next runtime. Only if user has limit to receive notifications. otherwise made the nextrun null. + // Update the schedule. + $DB->update_record('pulseaction_notification_sch', $update); + + if ($notifycount < $this->notificationdata->notifylimit + && $this->notificationdata->notifyinterval['interval'] != notification::INTERVALONCE) { + $newschedule = true; + $this->notification->create_schedule_foruser( + $this->schedule->userid, $notifiedtime, $notifycount, null, null, $newschedule); + } + } + } + } + + /** + * Fetch the queued schedules for the user. + * + * @param int|null $userid (Optional) The ID of the user. If provided, schedules are fetched and inti the notification sent. + * + * @return void + */ + protected function get_scheduled_records($userid=null) { + global $DB; + + $select[] = 'ns.id AS id'; // Set the schdule id as unique column. + + // Get columns not increase table queries. + // TODO: Fetch only used columns. Fetching all the fields in a query will make double the time of query result. + $tables = [ + 'ns' => $DB->get_columns('pulseaction_notification_sch'), + 'ai' => $DB->get_columns('pulse_autoinstances'), + 'pat' => $DB->get_columns('pulse_autotemplates'), + 'pati' => $DB->get_columns('pulse_autotemplates_ins'), + 'ni' => $DB->get_columns('pulseaction_notification_ins'), + 'an' => $DB->get_columns('pulseaction_notification'), + 'con' => array_fill_keys(["status", "additional", "isoverridden"], ""), + 'ue' => $DB->get_columns('user'), + 'c' => $DB->get_columns('course'), + 'ctx' => $DB->get_columns('context'), + 'cm' => array_fill_keys(["id", "course", "module", "instance"], ""), // Make the values as keys. + 'md' => array_fill_keys(['name'], "") + ]; + + foreach ($tables as $prefix => $table) { + $columns = array_keys($table); + // Columns. + array_walk($columns, function(&$value, $key, $prefix) { + $value = "$prefix.$value AS ".$prefix."_$value"; + }, $prefix); + + $select = array_merge($select, $columns); + } + // Final list of select columns, convert to sql mode. + $select = implode(', ', $select); + + // Number of notification to send in this que. + $limit = get_config('pulse', 'schedulecount') ?: 100; + + $userwhere = $userid ? ' AND ns.userid =:userid ' : ''; + $userparam = $userid ? ['userid' => $userid] : []; + + // Fetch the schedule which is status as 1 and nextrun not empty and not greater than now. + $sql = "SELECT $select FROM {pulseaction_notification_sch} ns + JOIN {pulse_autoinstances} AS ai ON ai.id = ns.instanceid + JOIN {pulse_autotemplates} AS pat ON pat.id = ai.templateid + JOIN {pulse_autotemplates_ins} AS pati ON pati.instanceid = ai.id + JOIN {pulseaction_notification_ins} AS ni ON ni.instanceid = ns.instanceid + JOIN {pulseaction_notification} AS an ON an.templateid = ai.templateid + JOIN {user} AS ue ON ue.id = ns.userid + JOIN {course} as c ON c.id = ai.courseid + JOIN {context} AS ctx ON ctx.instanceid = c.id AND ctx.contextlevel = 50 + LEFT JOIN {pulse_condition_overrides} AS con ON con.instanceid = pati.instanceid AND con.triggercondition = 'session' + LEFT JOIN {course_modules} AS cm ON cm.id = ni.dynamiccontent + LEFT JOIN {modules} AS md ON md.id = cm.module + JOIN ( + SELECT DISTINCT eu1_u.id, ej1_e.courseid, COUNT(ej1_ue.enrolid) AS activeenrolment + FROM {user} eu1_u + JOIN {user_enrolments} ej1_ue ON ej1_ue.userid = eu1_u.id + JOIN {enrol} ej1_e ON (ej1_e.id = ej1_ue.enrolid) + WHERE 1 = 1 AND ej1_ue.status = 0 + AND (ej1_ue.timestart = 0 OR ej1_ue.timestart <= :timestart) + AND (ej1_ue.timeend = 0 OR ej1_ue.timeend > :timeend) + GROUP BY eu1_u.id, ej1_e.courseid + ) AS active_enrols ON active_enrols.id = ue.id AND active_enrols.courseid = c.id + WHERE ns.status = :status AND an.status = 1 + AND active_enrols.activeenrolment <> 0 + AND c.visible = 1 + AND c.startdate <= :startdate AND (c.enddate = 0 OR c.enddate >= :enddate) + AND ue.deleted = 0 AND ue.suspended = 0 + AND ns.suppressreached = 0 AND ns.scheduletime <= :current_timestamp $userwhere ORDER BY ns.timecreated ASC"; + + $params = [ + 'status' => notification::STATUS_QUEUED, + 'current_timestamp' => time(), + 'timestart' => time(), 'timeend' => time(), + 'startdate' => time(), 'enddate' => time() + ] + $userparam; + + $schedules = $DB->get_records_sql($sql, $params, 0, $limit); + + return $schedules; + } + + /** + * Retrieves dynamic content modules for a list of schedules. + * + * @param array $schedules An array of schedules containing information about modules. + * Each schedule should have 'md_name' (module name) and 'cm_instance' (module instance) properties. + * + * @return array An array of dynamic modules data. + */ + protected function get_modules_dynamic_content($schedules) { + // Get the dynamic modules list of all schedules. + $dynamicmodules = []; + foreach ($schedules as $key => $schedule) { + if (!isset($schedule->md_name) || empty($schedule->md_name)) { + continue; + } + $dynamicmodules[$schedule->md_name][] = $schedule->cm_instance; + } + + return notification::get_modules_data($dynamicmodules); + } + + /** + * Builds the values for a given schedule. + * + * @param object $schedule The schedule object containing information about the automation instance. + * + * @return void + */ + protected function build_schedule_values($schedule) { + + $this->schedulerecord = $schedule; + + // Prepare templates instance data. + $templatedata = helper::filter_record_byprefix($schedule, 'pat'); + $templateinsdata = helper::filter_record_byprefix($schedule, 'pati'); + $templateinsdata = (object) helper::merge_instance_overrides($templateinsdata, $templatedata); + $templateinsdata->triggerconditions = json_decode($templateinsdata->triggerconditions, true); + + // Prepare the instance data. + $instancedata = (object) helper::filter_record_byprefix($schedule, 'ai'); + // Merge the template data to instance. + $instancedata->template = $templateinsdata; + unset($templateinsdata->id); + $instancedata = (object) array_merge((array) $instancedata, (array) $templateinsdata); + $this->instancedata = $instancedata; // Auomtaion instance data. + + if (isset($this->conditions[$instancedata->id])) { + // Include the condition for this instance if already created for this cron use it. + $this->instancedata->condition = $this->conditions[$instancedata->id]; + } else { + $condition = (new instances($instancedata->id))->include_conditions_data($this->instancedata); + $this->conditions[$instancedata->id] = $condition; + $this->instancedata->condition = $condition; + } + + // Schedule data. + $this->schedule = (object) helper::filter_record_byprefix($schedule, 'ns'); + // Course data. + $this->course = (object) helper::filter_record_byprefix($schedule, 'c'); + // User data. + $this->user = (object) helper::filter_record_byprefix($schedule, 'ue'); + // Course context data. + $context = (object) helper::filter_record_byprefix($schedule, 'ctx'); + // Conver the context data to moodle context. + $this->coursecontext = \mod_pulse_context_course::create_instance_fromrecord($context); + // Filter the notification data by its prefix. + $notificationrecord = helper::filter_record_byprefix($schedule, 'an'); + // Filter the notification instance data by its prefix. + $notificationinstancerecord = helper::filter_record_byprefix($schedule, 'ni'); + + // Merge the notification overrides data and its notification data. + $this->notificationdata = (object) helper::merge_instance_overrides($notificationinstancerecord, $notificationrecord); + + // Filter the notification instance overrided values list. + $this->notificationoverrides = array_filter((array) $notificationinstancerecord, function($value) { + return $value !== null; + }); + + // Create notification instance. + // Set the notification instance data merge with notification and instance data. + $this->notification = notification::instance($notificationrecord['id']); + $this->notification->set_notification_data($this->notificationdata, $this->instancedata); + + } + + /** + * Finds the sender user for this schedule. + * + * @return mixed Returns either a string with the custom sender email, or an object representing the sender user. + */ + protected function find_sender_user() { + // Find the sender for this schedule. + if ($this->notificationdata->sender == notification::SENDERCUSTOM) { + // Use the custom sender email as the support user email. + $sender = $this->notificationdata->senderemail; + } else if ($this->notificationdata->sender == notification::SENDERTENANTROLE) { + $sender = $this->notification->get_tenantrole_sender($this->schedulerecord); + } else { + // Get user groups is sender is configured as group teacher. + $groups = ($this->notificationdata->sender == notification::SENDERGROUPTEACHER) + ? groups_get_user_groups($this->course->id, $this->schedule->userid) : 0; + + $groupids = 0; + if (!empty($groups)) { + $firstgroup = current($groups); + $groupids = current($firstgroup); + } + // Get the course teacher if group teacher not available it will fallback to course teacher automatically. + $sender = (object) $this->get_sender_users($groupids); + + if (empty((array) $sender)) { + // Sender not found then use the support user. + $sender = $this->supportuser; + } + } + + return $sender; + } + + /** + * Retrieves the sender user(s) for a given group ID. + * + * @param int|array $groupid The ID of the group or an array of group IDs. + * + * @return stdClass|array Returns an object representing the sender user if found, or an empty array if not. + */ + protected function get_sender_users($groupid) { + + $groupid = is_array($groupid) ? current($groupid) : $groupid; + + $withcapability = 'pulseaction/notification:sender'; + $sender = get_enrolled_users( + $this->coursecontext, + $withcapability, + $groupid, + 'u.*', + null, + 0, + 1, + true + ); + + return !empty($sender) ? current($sender) : []; + } + + /** + * Prepares module data to use as placeholders in notifications. + * + * @param array $modules An array of dynamic content modules organized by module name and instance. + * @param object $cmdata An object containing information about the module (modname, instance, id). + * + * @return stdClass Returns an object representing the module data to be used as placeholders. + */ + public function prepare_moduledata_placeholders($modules, $cmdata) { + global $CFG; + + // Prepare the module data to use as placeholders. + $mod = new \stdclass; + + // Find the module data if dynamic content is configured. + if ($modules && $this->notificationdata->dynamiccontent) { + $mod = (object) $modules[$cmdata->modname][$cmdata->instance] ?? []; + } + + return $mod; + } + + /** + * Includes session data in the module object. + * + * @param stdClass $mod The module object to which session data will be added. + * @param stdClass $sessiondata An object containing session data. + * @param int $userid The ID of the user. + * + * @return stdClass The modified module object with session data included. + */ + public function include_session_data(&$mod, $sessiondata, $userid) { + global $CFG; + + require_once($CFG->dirroot.'/mod/facetoface/lib.php'); + + $modules = $sessiondata->modules; + $sessions = \pulsecondition_session\conditionform::get_session_data($modules, $userid); + if (empty($sessions)) { + $mod->session = new stdClass(); + } else { + $finalsessiondata = new \stdclass(); + $session = current($sessions); + $finalsessiondata->discountcode = $session->discountcode; + $finalsessiondata->details = format_text($session->details); + $finalsessiondata->capacity = $session->capacity; + $finalsessiondata->normalcost = format_cost($session->normalcost); + $finalsessiondata->discountcost = format_cost($session->discountcost); + + $formatedtime = facetoface_format_session_times($session->timestart, $session->timefinish, null); + $finalsessiondata = (object) array_merge((array) $finalsessiondata, (array) $formatedtime); + + $customfields = facetoface_get_session_customfields(); + $finalsessiondata->customfield = new \stdclass(); + foreach ($customfields as $field) { + $finalsessiondata->customfield->{$field->shortname} = facetoface_get_customfield_value( + $field, $session->sessionid, 'session'); + } + + $mod->session = $finalsessiondata; + } + + return $mod; + } +} diff --git a/actions/notification/classes/task/notify_users.php b/actions/notification/classes/task/notify_users.php new file mode 100644 index 0000000..d2c6776 --- /dev/null +++ b/actions/notification/classes/task/notify_users.php @@ -0,0 +1,178 @@ +. + +/** + * Scheduled cron task to send pulse. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification\task; + +use pulseaction_notification\schedule; +use pulseaction_notification\notification; +use tool_dataprivacy\form\context_instance; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/pulse/lib.php'); + +/** + * Send notification to users - scheduled task execution observer. + */ +class notify_users extends \core\task\scheduled_task { + + /** + * Return the task's name as shown in admin screens. + * + * @return string + */ + public function get_name() { + return get_string('notifyusers', 'pulseaction_notification'); + } + + /** + * Cron execution to send the available pulses. + * + * @return void + */ + public function execute() { + schedule::instance()->send_scheduled_notification(); + } + + /** + * Module completion event observer. + * Find the notification which configured with this activity and disable the schedules for this user. + * + * @param stdclass $eventdata + * @return void + */ + public static function module_completed($eventdata) { + global $DB; + + // Event data. + $data = $eventdata->get_data(); + + $cmid = $data['contextinstanceid']; + $userid = $data['userid']; + + // Get the info for the context. + list($context, $course, $cm) = get_context_info_array($data['contextid']); + + // Course completion info. + $completion = new \completion_info($course); + + // Get all the notification instance configures the suppress with this activity. + $notifications = self::get_suppress_notifications($cmid); + + self::is_suppress_reached($notifications, $userid, $course, $completion); + } + + /** + * Find the scheduled notification instance supress conditions are reached for the user. + * + * @param array $notifications List of notification to verify the suppress. + * @param int $userid User ID to verify for. + * @param stdclass $course Instance Course record. + * @param \completion_info $completion Instance course completion info. + * @return bool True if the user is reached the suppress conditions for the instance. Otherwise False. + */ + public static function is_suppress_reached($notifications, $userid, $course, $completion=null) { + global $DB; + + $completion = $completion ?: new \completion_info($course); + + foreach ($notifications as $notification) { + // Get the notification suppres module ids. + $suppress = $notification->suppress ? json_decode($notification->suppress) : ''; + + if (!empty($suppress)) { + $result = []; + // Find the completion status for all this suppress modules. + foreach ($suppress as $cmid) { + if (method_exists($completion, 'get_completion_data')) { + $modulecompletion = $completion->get_completion_data($cmid, $userid, []); + } else { + $cminfo = get_coursemodule_from_id('', $cmid); + $modulecompletion = (array) $completion->get_data($cminfo, false, $userid); + } + if ($modulecompletion['completionstate'] == COMPLETION_COMPLETE) { + $result[] = true; + } + } + + // If suppress operator set as all, check all the configures modules are completed. + if ($notification->suppressoperator == \mod_pulse\automation\action_base::OPERATOR_ALL) { + // Remove the schedule only if all the activites are completed. + if (count($result) == count($suppress)) { + $remove = true; + } + + } else { + // If any one of the activity is completed then remove the schedule from the user. + if (count($result) >= 1) { + $remove = true; + } + } + + // Update the flag to user schedules as suppress reached, it prevents the update of the schedule on notification. + if (isset($remove) && $remove) { + $remove = false; // Reset for the next notification test. + + $sql = "SELECT * FROM {pulseaction_notification_sch} + WHERE instanceid = :instanceid AND userid = :userid + AND (status = :disabledstatus OR status = :queued)"; + + $params = [ + 'instanceid' => $notification->instanceid, 'userid' => $userid, + 'disabledstatus' => notification::STATUS_DISABLED, 'queued' => notification::STATUS_QUEUED + ]; + + if ($record = $DB->get_record_sql($sql, $params)) { + $DB->set_field('pulseaction_notification_sch', 'suppressreached', notification::SUPPRESSREACHED, + ['id' => $record->id] + ); + } + + $suppressreached[$notification->id] = notification::SUPPRESSREACHED; + } + } + + return $suppressreached ?? false; + } + } + + /** + * Retrieves notifications with suppression value containing a specific ID. + * + * @param int $id The ID to search for within the suppression values. + * + * @return array An array of notification records matching the suppression criteria. + */ + public static function get_suppress_notifications($id) { + global $DB; + + $like = $DB->sql_like('suppress', ':value'); + $sql = "SELECT * FROM {pulseaction_notification_ins} WHERE $like"; + $params = ['value' => '%"'.$id.'"%']; + + $records = $DB->get_records_sql($sql, $params); + + return $records; + } +} diff --git a/actions/notification/db/access.php b/actions/notification/db/access.php new file mode 100644 index 0000000..bc332f4 --- /dev/null +++ b/actions/notification/db/access.php @@ -0,0 +1,47 @@ +. + +/** + * Define plugin capabilities. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + // Capability to recieve notifications. + 'pulseaction/notification:receivenotification' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'guest' => CAP_ALLOW, + ) + ), + + 'pulseaction/notification:sender' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW + ), + ), + +); diff --git a/actions/notification/db/events.php b/actions/notification/db/events.php new file mode 100644 index 0000000..6c57bd1 --- /dev/null +++ b/actions/notification/db/events.php @@ -0,0 +1,31 @@ +. + +/** + * Pulseaction notification - Define event observers. + * + * @package pulseaction_notification + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + array( + 'eventname' => 'core\event\course_module_completion_updated', + 'callback' => '\pulseaction_notification\task\notify_users::module_completed', + ), +]; diff --git a/actions/notification/db/install.xml b/actions/notification/db/install.xml new file mode 100644 index 0000000..e96b5ca --- /dev/null +++ b/actions/notification/db/install.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+
diff --git a/actions/notification/db/services.php b/actions/notification/db/services.php new file mode 100644 index 0000000..d2ee5b7 --- /dev/null +++ b/actions/notification/db/services.php @@ -0,0 +1,37 @@ +. + +/** + * Pulse action notification - external services. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + + 'pulseaction_notification_get_chapters' => array( + 'classname' => 'pulseaction_notification\external', + 'methodname' => 'get_chapters', + 'description' => 'Get list of chapters for the selected book', + 'type' => 'write', + 'ajax' => true, + 'loginrequired' => true, + ), +); diff --git a/actions/notification/db/tasks.php b/actions/notification/db/tasks.php new file mode 100644 index 0000000..4340914 --- /dev/null +++ b/actions/notification/db/tasks.php @@ -0,0 +1,37 @@ +. + +/** + * Pulse action notification - List of scheduled tasks to send pulses in background. + * + * @package pulseaction_notification + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('No direct access !'); + +$tasks = [ + [ + 'classname' => 'pulseaction_notification\task\notify_users', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + ], +]; diff --git a/actions/notification/lang/en/pulseaction_notification.php b/actions/notification/lang/en/pulseaction_notification.php new file mode 100644 index 0000000..1dd347f --- /dev/null +++ b/actions/notification/lang/en/pulseaction_notification.php @@ -0,0 +1,139 @@ +. + +/** + * Notification pulse action - Language strings defined. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Pulse notifications'; + +// ...Capabilities. +$string['notification:receivenotification'] = 'Recevie notifications from pulse'; +$string['notification:sender'] = 'Sender of the automation notification'; + + +$string['courseteacher'] = 'Course teacher'; +$string['groupteacher'] = 'Group teacher'; +$string['tenantrole'] = 'Tenant role'; +$string['custom'] = 'Custom'; +$string['senderemail'] = 'Sender email'; +$string['once'] = 'Once'; +$string['daily'] = 'Daily'; +$string['weekly'] = 'Weekly'; +$string['monthly'] = 'Monthly'; +$string['monday'] = 'Monday'; +$string['tuesday'] = 'Tuesday'; +$string['wednesday'] = 'Wednesday'; +$string['thursday'] = 'Thursday'; +$string['friday'] = 'Friday'; +$string['saturday'] = 'Saturday'; +$string['sunday'] = 'Sunday'; +$string['none'] = 'None'; +$string['before'] = 'Before'; +$string['after'] = 'After'; + +$string['suppressnotification'] = 'Suppress notification'; +$string['description'] = 'Description'; +$string['content'] = 'Content'; +$string['teaser'] = 'Teaser'; +$string['full_linked'] = 'Full linked'; +$string['full_not_linked'] = 'Full not linked'; + +// ...Form tab. +$string['formtab'] = 'Notification'; +$string['notifyusers'] = 'Send notification'; + +// ... Report builder. +$string['notificationreport'] = 'Pulse Schedules'; +$string['timecreated'] = 'Time created'; +$string['nextrun'] = 'Datetime to send notification'; +$string['status'] = 'Status'; +$string['subject'] = 'Subject'; +$string['messagetype'] = 'Message type'; +// ...Status of the schedule. +$string['queued'] = 'Queued'; +$string['sent'] = 'sent'; +$string['failed'] = 'Failed'; +$string['onhold'] = 'On hold'; +$string['schedulecreatedtime'] = 'Schedule created time'; +$string['scheduledtime'] = 'Scheduled time'; +$string['cohort'] = 'Cohort'; +$string['readmore'] = 'Read more'; +$string['instanceid'] = 'Instance ID'; + +// Help texts. +// Sender. +$string['sender'] = 'Sender'; +$string['sender_help'] = 'Choose the sender of the notification from the following options:
Course Teacher: The notification will be sent from the course teacher (the first one assigned if there are several). If the user is not in any group, it falls back to the site support contact. Note that this is determined by capability, not by an actual role.
Group Teacher: The notification will be sent from the non-editing teacher who is a member of the same group as the user (the first one assigned if there are several). If there\'s no non-editing teacher in the group, it falls back to the course teacher. Note that this is determined by capability, not by an actual role.
Tenant Role (Workplace Feature): The notification will be sent from the user assigned to the specified role in the tenant (the first one assigned if there are several). If there\'s no user with the selected role, it falls back to the site support contact. Note that this is determined by capability, not by an actual role.
Custom: If selected, an additional setting for "Sender Email" will be displayed where you can enter a specific email address to be used as the sender.'; +// Interval. +$string['interval'] = 'Interval'; +$string['interval_help'] = 'Choose the interval for sending notifications:
Once: Send the notification only one time.
Daily: Send the notification every day at the time selected below.
Weekly: Send the notification every week on the day of the week and time of below.
Monthly: Send the notification every month on the day of the month and time of below.'; +// Delay. +$string['delay'] = 'Delay'; +$string['delay_help'] = 'Choose the delay option for sending notifications: +
None: Send notifications immediately upon the condition being met, considering the schedule limitations (e.g., weekday or time of day).
Before: Send the notification a specified number of days/hours before the condition is met. Note that this is only possible for timed events, e.g., appointment sessions.
After: Send the notification a specified number of days/hours after the condition is met. This is possible for all conditions.'; +// Delay duration. +$string['delayduraion'] = 'Delay duraion'; +$string['delayduraion_help'] = 'Please enter the duration time for the delay in sending the notification. This duration should be specified in terms of days or hours, depending on the selected delay option.'; +// Limit. +$string['limit'] = 'Limit of the notifications'; +$string['limit_help'] = 'Enter a number to limit the total number of notifications sent.
Note:Enter "0" for no limit. This is only relevant if the schedule is not set to "Once".'; +// Recipients. +$string['recipients'] = 'Recipients'; +$string['recipients_help'] = 'Select one or more roles that have the capability to receive notifications. By default, it\'s set for all graded roles, including students. Users selected here will be used in the query to determine who gets notifications.'; +// CC recipients. +$string['ccrecipients'] = 'Cc '; +$string['ccrecipients_help'] = 'Select course context and user context roles that will receive the notification as a CC (Carbon Copy) to the main recipient. Course context roles determine users by enrolment in the course and membership of a group, while user context roles determine users by their relation to the recipient (assigned role in user).'; +// BCC recipients. +$string['bccrecipients'] = 'Bcc '; +$string['bccrecipients_help'] = 'Select course context and user context roles that will receive the notification as a BCC (Blind Carbon Copy) to the main recipient. Course context roles determine users by enrolment in the course and membership of a group, while user context roles determine users by their relation to the recipient (assigned role in user).'; +// Subject. +$string['subject'] = 'Subject'; +$string['subject_help'] = 'Enter the subject for the notification.'; +// Header content. +$string['headercontent'] = 'Header content'; +$string['headercontent_help'] = 'Enter the first part of the body for the notification. This field supports filters and placeholders.'; +// Static content. +$string['staticcontent'] = 'Static content'; +$string['staticcontent_help'] = 'Enter the second part of the body for the notification. This field supports filters and placeholders.'; +// Footer content. +$string['footercontent'] = 'Footer content'; +$string['footercontent_help'] = 'Enter the last part of the body for the notification. This field supports filters and placeholders.'; +// Preview. +$string['preview'] = 'Preview'; +$string['preview_help'] = 'Click this button to open a modal window that displays the notification, allowing you to select an example user to determine the content of the notification.'; +// Suppress module. +$string['suppressmodule'] = 'Suppress module'; +$string['suppressmodule_help'] = 'Choose one or more activities that, when completed, will suppress the notification from being sent. You can select the operand below to determine how these activities affect notification.'; +// Suppress operator. +$string['suppressoperator'] = 'Suppress operator'; +$string['suppressoperator_help'] = 'Choose the operand that determines how the selected activities completion affects the notification:
Any: If any of the selected activities above are completed, the notification shall not be sent.
All: If all of the selected activities above are completed, the notification shall not be sent.'; +// Dynamic content. +$string['dynamiccontent'] = 'Dynamic content'; +$string['dynamiccontent_help'] = 'Select an activity within the course to add content below the static content. This is only available in the automation instance within the course.'; +// Content type. +$string['contenttype'] = 'Content type'; +$string['contenttype_help'] = 'Choose the type of content to be added below the static content:
Description: If selected, the description of the selected activity shall be added to the body of the notification.
Content: If selected, the content of the selected activity shall be added to the body of the notification. Note that this should support specific mod types like Page and Book with the ability to select specific chapters.'; +// Content length. +$string['contentlength'] = 'Content length'; +$string['contentlength_help'] = ' Choose the content length to include in the notification:
Teaser: If selected, only the first paragraph shall be used, with a "Read More" link added after it.
Full, Linked: If selected, the entire content shall be used, with a link to the content after it.
Full, Not Linked: If selected, the entire content shall be used without a link to the content after it.'; +// Chapters. +$string['chapters'] = 'Chapters'; +$string['chapters_help'] = 'Provides support to select specific chapters from a Book activity.'; diff --git a/actions/notification/lib.php b/actions/notification/lib.php new file mode 100644 index 0000000..c9a3318 --- /dev/null +++ b/actions/notification/lib.php @@ -0,0 +1,185 @@ +. + +/** + * Notification pulse action - Library file contains commonly used functions. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use pulseaction_notification\notification; +use pulseaction_notification\schedule; + +/** + * Returns list of fileareas used in the pulsepro reminder contents. + * + * @return array list of filearea to support pluginfile. + */ +function pulseaction_notification_extend_pulse_filearea() : array { + + return [ + 'pulsenotification_headercontent', + 'pulsenotification_staticcontent', + 'pulsenotification_footercontent', + 'pulsenotification_headercontent_instance', + 'pulsenotification_staticcontent_instance', + 'pulsenotification_footercontent_instance' + ]; +} + +/** + * Updates chapters for a notification output fragment. + * + * @param array $args An associative array of arguments. + * - 'context' (object) The context object. + * - 'mod' (int) The Course Module ID (optional). + * + * @return mixed Returns the loaded book chapters or null if 'mod' is not set in the arguments. + */ +function pulseaction_notification_output_fragment_update_chapters($args) { + $context = $args['context']; + + if (isset($args['mod'])) { + $cmid = $args['mod']; + return pulseaction_notification\notification::load_book_chapters($cmid); + } +} + +/** + * Preview of the instance for sepecific user. + * + * @param array $args + * @return string + */ +function pulseaction_notification_output_fragment_preview_instance_content($args) { + global $OUTPUT; + + $context = $args['context']; + if (isset($args['instanceid'])) { + + $insobj = new \mod_pulse\automation\instances($args['instanceid']); + $formdata = (object) $insobj->get_instance_data(); + + $notificationid = $formdata->actions['notification']['id']; + $notificationobj = pulseaction_notification\notification::instance($notificationid); + + $notificationobj->set_notification_data($formdata->actions['notification'], $formdata); + $content = $notificationobj->build_notification_content(null, null, $formdata->override); + + $sender = core_user::get_support_user(); + $users = get_enrolled_users(\context_course::instance($formdata->courseid)); + $user = (object) ($args['userid'] != null ? core_user::get_user($args['userid']) : current($users)); + + $course = get_course($formdata->courseid ?? SITEID); + + $mod = new stdclass; + // TODO: Inlcude the vars update from condition plugins. + if ($formdata->actions['notification']['dynamiccontent']) { + // Prepare the module data. based on dynamic content and includ the session data. + $modname = $formdata->actions['notification']['mod']->modname; + $dynamicmodules[$modname][] = $formdata->actions['notification']['mod']->instance; + $modules = notification::get_modules_data($dynamicmodules); + $mod = current($modules[$modname]); + } + // Check the session condition are set for this notification. if its added then load the session data for placeholders. + $sessionincondition = in_array('session', (array) array_keys($formdata->condition)); + if ($sessionincondition && $formdata->condition['session']['status']) { + $sessionconditiondata = (object) ['modules' => $formdata->condition['session']['modules']]; + schedule::instance()->include_session_data($mod, $sessionconditiondata, $user->id); + } + + list($subject, $messagehtml) = mod_pulse\helper::update_emailvars($content, '', $course, $user, $mod, $sender); + $selector = ""; + + $data = ['message' => $messagehtml, 'usersselector' => $selector]; + + return $OUTPUT->render_from_template('pulseaction_notification/preview', ['data' => $data]); + } +} + +/** + * Preview the content of the instance. + * + * @param [type] $args + * @return void + */ +function pulseaction_notification_output_fragment_preview_content($args) { + global $OUTPUT; + + $coursecontext = $args['context']; + + if (isset($args['contentheader'])) { + $course = get_course($args['courseid'] ?? $coursecontext->instanceid); + + // Get the enrolled users for this course. + $users = get_enrolled_users($coursecontext); + $list = []; + foreach ($users as $userid => $user) { + $list[$userid] = fullname($user); + } + $sender = core_user::get_support_user(); + + $user = (object) ($args['userid'] != null ? core_user::get_user($args['userid']) : current($users)); + + $dynamiccontent = ''; + if (isset($args['contentdynamic']) && !empty($args['contentdynamic'])) { + + $module = get_coursemodule_from_id('', $args['contentdynamic']); + $moddata = (object) [ + 'instance' => $module->instance, + 'modname' => $module->modname, + 'id' => $module->id, + ]; + $context = \context_module::instance($module->id); + // Generate dynamic content for the instance. + $dynamiccontent = notification::generate_dynamic_content( + $args['contenttype'], + $args['contentlength'], + $args['chapterid'], + $context, + $moddata + ); + $mod = new stdclass; + // TODO: Inlcude the vars update from condition plugins. + if ($args['contentdynamic']) { + // Prepare the module data. based on dynamic content and includ the session data. + $modname = $module->modname; + $dynamicmodules[$modname][] = $module->instance; + $modules = notification::get_modules_data($dynamicmodules); + $mod = current($modules[$modname]); + } + // Check the session condition are set for this notification. if its added then load the session data for placeholders. + parse_str($args['formdata'], $formdata); + $sessionincondition = in_array('session', (array) array_keys($formdata['condition'])); + if ($sessionincondition && $formdata['condition']['session']['status']) { + $sessionconditiondata = (object) ['modules' => $formdata['condition']['session']['modules']]; + schedule::instance()->include_session_data($mod, $sessionconditiondata, $user->id); + } + } + + $content = $args['contentheader'] . $args['contentstatic'] . $dynamiccontent . $args['contentfooter']; + + // Update the placeholders with course and user data. + list($subject, $messagehtml) = mod_pulse\helper::update_emailvars($content, '', $course, $user, $mod, $sender); + // User selector. + $selector = html_writer::select($list, 'userselector', $user->id); + + $data = ['message' => $messagehtml, 'usersselector' => $selector]; + return $OUTPUT->render_from_template('pulseaction_notification/preview', ['data' => $data]); + } +} diff --git a/actions/notification/templates/preview.mustache b/actions/notification/templates/preview.mustache new file mode 100644 index 0000000..7370760 --- /dev/null +++ b/actions/notification/templates/preview.mustache @@ -0,0 +1,17 @@ +

+
+ {{#data.usersselector}} +
+
+ {{#str}} users, core {{/str}} +
+
+ {{{data.usersselector}}} +
+
+ {{/data.usersselector}} +
+
+ {{{data.message}}} +
+
diff --git a/actions/notification/tests/behat/behat_pulseaction_notification.php b/actions/notification/tests/behat/behat_pulseaction_notification.php new file mode 100644 index 0000000..2e192bf --- /dev/null +++ b/actions/notification/tests/behat/behat_pulseaction_notification.php @@ -0,0 +1,106 @@ +. + +/** + * Behat pulse action notification - related steps definitions. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php'); + +use Behat\Gherkin\Node\TableNode as TableNode, + Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\DriverException as DriverException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; +use PhpOffice\PhpSpreadsheet\Worksheet\Table; + +/** + * Pulse notification automation - related steps definitions. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_pulseaction_notification extends behat_base { + + /** + * Fills a automation template condition form with field/value data. + * + * @Given /^I create pulse notification template "([^"]*)" "([^"]*)" to these values:$/ + * @param string $title + * @param string $reference + * @param TableNode $notificationdata + */ + public function i_create_pulsenotification_template_with_general($title, $reference, $notificationdata) { + + $this->execute("behat_general::i_click_on", ["Create new template", "button"]); + $this->execute('behat_forms::i_set_the_field_to', ["Title", $title]); + $this->execute('behat_forms::i_set_the_field_to', ["Reference", $reference]); + $this->execute("behat_general::i_click_on_in_the", ["Notification", "link", "#automation-tabs", "css_element"]); + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', [$notificationdata]); + $this->execute("behat_general::i_click_on", ["Save changes", "button"]); + } + + /** + * Fills a automation template condition form with field/value data. + * + * @Given /^I create pulse notification instance "([^"]*)" "([^"]*)" to these values:$/ + * @param string $title + * @param string $reference + * @param TableNode $notificationdata + */ + public function i_create_pulsenotification_instance_with_general($title, $reference, TableNode $notificationdata) { + + $this->execute('behat_forms::i_set_the_field_to', ["templateid", $title]); + $this->execute("behat_general::i_click_on", ["Add automation instance", "button"]); + $this->execute('behat_forms::i_set_the_field_to', ["Reference", $reference]); + $this->execute("behat_general::i_click_on_in_the", ["Notification", "link", "#automation-tabs", "css_element"]); + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', [$notificationdata]); + $this->execute("behat_general::i_click_on", ["Save changes", "button"]); + } + + /** + * Fills a automation template condition form with field/value data. + * + * @Given /^I initiate new automation template to these values:$/ + * @throws ElementNotFoundException Thrown by behat_base::find + * @param TableNode $generaldata + */ + public function i_set_pulsenotification_template_with_general($generaldata) { + $this->execute('behat_pulse::i_navigate_to_automation_template'); + $this->execute("behat_general::i_click_on", ["Create new template", "button"]); + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', [$generaldata]); + } + + /** + * Fills a automation template condition form with field/value data. + * + * @Given /^I set previous automation template notification to these values:$/ + * @throws ElementNotFoundException Thrown by behat_base::find + * @param TableNode $notificationdata + */ + public function i_set_previous_template_notification($notificationdata) { + + $this->execute("behat_general::i_click_on_in_the", ["Notification", "link", "#automation-tabs .nav-item", "css_element"]); + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', [$notificationdata]); + + } +} diff --git a/actions/notification/tests/behat/pulse_notification_template.feature b/actions/notification/tests/behat/pulse_notification_template.feature new file mode 100644 index 0000000..5797ba9 --- /dev/null +++ b/actions/notification/tests/behat/pulse_notification_template.feature @@ -0,0 +1,136 @@ +@mod @mod_pulse @mod_pulse_automation @mod_pulse_automation_template @pulseactions @pulseaction_notification_template +Feature: Configuring the pulseaction_notification plugin on the "Automation template" page, applying different configurations to the notification + In order to use the features + As admin + I need to be able to configure the pulse automation template + + Background: + Given the following "categories" exist: + | name | category | idnumber | + | Cat 1 | 0 | CAT1 | + And the following "course" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | student | User 1 | student1@test.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + + @javascript + Scenario: Create notification template and instance + Given I log in as "admin" + And I navigate to automation templates + And I create pulse notification template "WELCOME MESSAGE" "WELCOMEMESSAGE_" to these values: + | Sender | Course teacher | + | Recipients | Student | + | Subject | Welcome to {Site_FullName} | + | Header content | Hi {User_firstname} {User_lastname},
Welcome to learning portal of {Site_FullName} | + | Footer content | Copyright @ 2023 {Site_FullName} | + Then I should see "Automation templates" + And I should see "WELCOME MESSAGE" in the "pulse_automation_template" "table" + And I navigate to course "Course 1" automation instances + And I create pulse notification instance "WELCOME MESSAGE" "COURSE_1" to these values: + | Recipients | Student | + And I should see "WELCOMEMESSAGE_COURSE_1" in the "pulse_automation_template" "table" + And I click on ".action-report" "css_element" in the "WELCOMEMESSAGE_COURSE_1" "table_row" + And I switch to a second window + And the following should exist in the "reportbuilder-table" table: + | Full name | Subject | Status | + | student User 1 | Welcome to Acceptance test site | Queued | + + @javascript + Scenario: Override notification template + Given I log in as "admin" + And I navigate to automation templates + And I create pulse notification template "WELCOME MESSAGE" "WELCOMEMESSAGE_" to these values: + | Sender | Course teacher | + | Recipients | Student | + | Subject | Welcome to {Site_FullName} | + | Header content | Hi {User_firstname} {User_lastname},
Welcome to learning portal of {Site_FullName} | + | Footer content | Copyright @ 2023 {Site_FullName} | + Then I should see "Automation templates" + And I should see "WELCOME MESSAGE" in the "pulse_automation_template" "table" + And I navigate to course "Course 1" automation instances + And I create pulse notification instance "WELCOME MESSAGE" "COURSE_1" to these values: + | override[subject] | 1 | + | Subject | Welcome to learning portal {Site_FullName} | + And I should see "WELCOMEMESSAGE_COURSE_1" in the "pulse_automation_template" "table" + And I click on ".action-report" "css_element" in the "WELCOMEMESSAGE_COURSE_1" "table_row" + And I switch to a second window + And the following should exist in the "reportbuilder-table" table: + | Full name | Subject | Status | + | student User 1 | Welcome to learning portal Acceptance test site | Queued | + + # @javascript + # Scenario: Create notification template and instance + # Given I log in as "admin" + # And I navigate to automation templates + # And I create pulse notification template "WELCOME MESSAGE" "WELCOMEMESSAGE_" to these values: + # | Sender | Course teacher | + # | Recipients | Student | + # | Subject | Welcome to {Site_FullName} | + # | Header content | Hi {User_firstname} {User_lastname},
Welcome to learning portal of {Site_FullName} | + # | Footer content | Copyright @ 2023 {Site_FullName} | + # Then I should see "Automation templates" + # And I should see "WELCOME MESSAGE" in the "pulse_automation_template" "table" + # And I navigate to course "Course 1" automation instances + # And I create pulse notification instance "WELCOME MESSAGE" "COURSE_1" to these values: + # | override[subject] | 1 | + # | Subject | Welcome to learning portal {Site_FullName} | + # And I should see "WELCOMEMESSAGE_COURSE_1" in the "pulse_automation_template" "table" + + # And I click on "Create new template" "button" + # Then I should see "Notification" in the "#automation-tabs .nav-item .nav-link[href='#pulse-action-notification']" "css_element" + + # And I set the following fields to these values: + # | Title | WELCOME MESSAGE | + # | Reference | WELCOMEMESSAGE_ | + # And I click on "Notification" "link" in the "#automation-tabs .nav-item" "css_element" + # And I set the following fields to these values: + # | Sender | Course teacher | + # | Recipients | Student | + # | Subject | Welcome to {Site_FullName} | + # | Header content | Hi {User_firstname} {User_lastname},
Welcome to learning portal of {Site_FullName} | + # | Footer content | Copyright @ 2023 {Site_FullName} | + # Then I press "Save changes" + # Then I should see "Automation templates" + # And I should see "WELCOME MESSAGE" in the "pulse_automation_template" "table" + # And I navigate to course "Course 1" automation instances + # Then I click on "Add automation instance" "button" + # And I click on "Notification" "link" in the "#automation-tabs .nav-item" "css_element" + # And I set the following fields to these values: + # | insreference | COURSE_1 | + # Then I press "Save changes" + # And I should see "WELCOMEMESSAGE_COURSE_1" in the "pulse_automation_template" "table" + + # @javascript + # Scenario: Verify instance override in template + # Given I log in as "admin" + # And I navigate to automation templates + # And I click on "Create new template" "button" + # Then I should see "Notification" in the "#automation-tabs .nav-item .nav-link[href='#pulse-action-notification']" "css_element" + # And I set the following fields to these values: + # | Title | WELCOME MESSAGE | + # | Reference | WELCOMEMESSAGE_ | + # And I click on "Notification" "link" in the "#automation-tabs .nav-item" "css_element" + # And I set the following fields to these values: + # | Sender | Course teacher | + # | Recipients | Student | + # | Subject | Welcome to {Site_FullName} | + # | Header content | Hi {User_firstname} {User_lastname},
Welcome to learning portal of {Site_FullName} | + # | Footer content | Copyright @ 2023 {Site_FullName} | + # Then I press "Save changes" + # Then I should see "Automation templates" + # And I should see "WELCOME MESSAGE" in the "pulse_automation_template" "table" + # And I navigate to course "Course 1" automation instances + # Then I click on "Add automation instance" "button" + # And I click on "Notification" "link" in the "#automation-tabs .nav-item" "css_element" + # And I set the following fields to these values: + # | insreference | COURSE_1 | + # And I click on "Notification" "link" in the "#automation-tabs .nav-item" "css_element" + # | override[subject] | 1 | + # | Subject | Welcome to learning portal {Site_FullName} | + # Then I press "Save changes" + # And I should see "WELCOMEMESSAGE_COURSE_1" in the "pulse_automation_template" "table" diff --git a/actions/notification/version.php b/actions/notification/version.php new file mode 100644 index 0000000..1d0b3ce --- /dev/null +++ b/actions/notification/version.php @@ -0,0 +1,28 @@ +. + +/** + * Notification pulse action - Component and version details. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = "pulseaction_notification"; +$plugin->version = 2023080407; diff --git a/amd/build/automation.min.js b/amd/build/automation.min.js new file mode 100644 index 0000000..e791951 --- /dev/null +++ b/amd/build/automation.min.js @@ -0,0 +1,3 @@ +define("mod_pulse/automation",["jquery","core/modal_factory","core/templates","core/str"],(function($,Modal,Template,Str){const moveOverRidePosition=function(){var group="checkboxgroupautomation";if(null===document.querySelectorAll("input[type=checkbox]."+group))return!0;document.querySelectorAll("input[type=checkbox]."+group).forEach((overElement=>{var id=overElement.id;id=id.replace("id_override_","");var element=document.querySelector("div#fitem_id_"+id);if(null===element&&null===(element=document.querySelector("div#fgroup_id_"+id)))return!0;var parent=overElement.parentNode;parent.innerHTML+='';var nodeToMove=document.createElement("div");nodeToMove.classList.add("custom-control","custom-switch"),nodeToMove.append(parent),element.querySelector(".felement").append(nodeToMove)})),function(){if(null===document.querySelectorAll("input[type=checkbox].checkboxgroupautomation")||null===document.querySelectorAll('[data-fieldtype="autocomplete"]'))return!0;document.querySelectorAll('[data-fieldtype="autocomplete"]').forEach((element=>{if(null===element)return!0;var observer=new MutationObserver((function(mutations){mutations.forEach((mutation=>{console.log(mutation),target=mutation.target;var overrideElement=target.querySelector(".custom-switch");null!==overrideElement&&(overrideElement.parentNode.append(overrideElement),observer.disconnect())}))}));observer.observe(element,{attributes:!0,childList:!0,subtree:!0})}))}()};return{init:function(){(()=>{if(null===document.forms["pulse-automation-template"])return!1;document.forms["pulse-automation-template"].onsubmit=e=>{var invalidElement=e.target.querySelector(".is-invalid");if(null===invalidElement)return!0;var hrefSelector='[href="#'+invalidElement.parentNode.parentNode.parentNode.id+'"]';document.querySelector(hrefSelector).click()}})(),function(){var templateReference=document.querySelector("#pulse-template-reference"),instanceReference=document.querySelector("#fitem_id_insreference .felement");templateReference&&instanceReference&&(templateReference.classList.remove("hide"),instanceReference.prepend(templateReference));const trigger=document.querySelectorAll('[data-target="overridemodal"]');null!==trigger&&trigger.forEach((elem=>{elem.nextSibling.querySelector(".felement").append(elem),elem.addEventListener("click",(function(e){e.preventDefault();var data=e.target.dataset,instance=document.querySelector("[name=overinstance_"+data.element+"]");if(null!==instance){var overrides=JSON.parse(instance.value);overrides.map((value=>(value.url=M.cfg.wwwroot+"/mod/pulse/automation/instances/edit.php?instanceid="+value.id+"&sesskey="+M.cfg.sesskey,value))),Modal.create({title:Str.get_string("instanceoverrides","pulse"),body:Template.render("mod_pulse/overrides",{instances:overrides})}).then((modal=>{modal.show()}))}}))}))}(),moveOverRidePosition(),null!==document.forms["pulse-automation-template"]&&(document.forms["pulse-automation-template"].onsubmit=e=>document.querySelector('[name="title"]').removeAttribute("disabled"))},instanceMenuLink:function(){(navMenu=>{if(null!==navMenu){var menu=navMenu.querySelector("a.automation-templates");null!==menu&&((menu=menu.parentNode).dataset.forceintomoremenu=!1,menu.querySelector("a").classList.remove("dropdown-item"),menu.querySelector("a").classList.add("nav-link"),menu.parentNode.removeChild(menu),navMenu.insertBefore(menu,navMenu.children[1]),window.dispatchEvent(new Event("resize")))}})(document.querySelector(".secondary-navigation ul.more-nav"))}}})); + +//# sourceMappingURL=automation.min.js.map \ No newline at end of file diff --git a/amd/build/automation.min.js.map b/amd/build/automation.min.js.map new file mode 100644 index 0000000..655c82c --- /dev/null +++ b/amd/build/automation.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"automation.min.js","sources":["../src/automation.js"],"sourcesContent":["define(\"mod_pulse/automation\", ['jquery', 'core/modal_factory', 'core/templates', 'core/str'], function($, Modal, Template, Str) {\r\n\r\n const moveOutMoreMenu = (navMenu) => {\r\n\r\n if (navMenu === null) {\r\n return;\r\n }\r\n\r\n var menu = navMenu.querySelector('a.automation-templates');\r\n\r\n if (menu === null) {\r\n return;\r\n }\r\n\r\n menu = menu.parentNode;\r\n menu.dataset.forceintomoremenu = false,\r\n menu.querySelector('a').classList.remove('dropdown-item');\r\n menu.querySelector('a').classList.add('nav-link');\r\n menu.parentNode.removeChild(menu);\r\n\r\n // Insert the stored menus before the more menu.\r\n navMenu.insertBefore(menu, navMenu.children[1]);\r\n window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu.\r\n };\r\n\r\n const returnToFailedTab = () => {\r\n\r\n if (document.forms['pulse-automation-template'] === null) {\r\n return false;\r\n }\r\n\r\n document.forms['pulse-automation-template'].onsubmit = (e) => {\r\n var form = e.target;\r\n var invalidElement = form.querySelector('.is-invalid');\r\n if (invalidElement === null) {\r\n return true;\r\n }\r\n\r\n var tabid = invalidElement.parentNode.parentNode.parentNode.id;\r\n var hrefSelector = '[href=\"#'+tabid+'\"]';\r\n\r\n document.querySelector(hrefSelector).click();\r\n }\r\n };\r\n\r\n // No need.\r\n const updateAutoCompletionPositions = function() {\r\n var group = \"checkboxgroupautomation\";\r\n\r\n if (document.querySelectorAll('input[type=checkbox].'+group) === null || document.querySelectorAll('[data-fieldtype=\"autocomplete\"]') === null) {\r\n return true;\r\n }\r\n\r\n document.querySelectorAll('[data-fieldtype=\"autocomplete\"]').forEach((element) => {\r\n\r\n if (element === null) {\r\n return true;\r\n }\r\n\r\n var observer = new MutationObserver(function(mutations) {\r\n mutations.forEach((mutation) => {\r\n console.log(mutation);\r\n // if(mutation.type === 'attributes') {\r\n target = mutation.target;\r\n var overrideElement = target.querySelector('.custom-switch');\r\n if (overrideElement === null) {\r\n return;\r\n }\r\n overrideElement.parentNode.append(overrideElement);\r\n observer.disconnect();\r\n })\r\n });\r\n observer.observe(element, { attributes: true, childList: true, subtree: true, });\r\n // observer.disconnect();\r\n })\r\n }\r\n\r\n const moveOverRidePosition = function() {\r\n\r\n var group = \"checkboxgroupautomation\";\r\n\r\n if (document.querySelectorAll('input[type=checkbox].'+group) === null) {\r\n return true;\r\n }\r\n\r\n document.querySelectorAll('input[type=checkbox].'+group).forEach((overElement) => {\r\n var id = overElement.id;\r\n id = id.replace('id_override_', '');\r\n var element = document.querySelector('div#fitem_id_'+id);\r\n if (element === null) {\r\n element = document.querySelector('div#fgroup_id_'+id);\r\n if (element === null) {\r\n return true;\r\n }\r\n }\r\n\r\n var parent = overElement.parentNode;\r\n parent.innerHTML += '';\r\n\r\n var nodeToMove = document.createElement('div');\r\n nodeToMove.classList.add('custom-control', 'custom-switch');\r\n nodeToMove.append(parent);\r\n element.querySelector(\".felement\").append(nodeToMove);\r\n });\r\n // Move the override button for autocompletion fields after the autocomplete nodes are created.\r\n updateAutoCompletionPositions();\r\n }\r\n\r\n /**\r\n * Create a modal to display the list of instances which is overriden the template setting.\r\n *\r\n * @returns {void}\r\n */\r\n const overrideModal = function() {\r\n\r\n // Add the template reference as prefix of the instance reference.\r\n var templateReference = document.querySelector('#pulse-template-reference');\r\n var instanceReference = document.querySelector('#fitem_id_insreference .felement');\r\n if (templateReference && instanceReference) {\r\n templateReference.classList.remove('hide');\r\n instanceReference.prepend(templateReference);\r\n }\r\n\r\n const trigger = document.querySelectorAll('[data-target=\"overridemodal\"]');\r\n\r\n if (trigger === null) {\r\n return;\r\n }\r\n\r\n trigger.forEach((elem) => {\r\n\r\n elem.nextSibling.querySelector('.felement').append(elem);\r\n\r\n elem.addEventListener('click', function(e) {\r\n e.preventDefault();\r\n var element = e.target;\r\n var data = element.dataset;\r\n var instance = document.querySelector('[name=overinstance_'+data.element+']');\r\n if (instance !== null) {\r\n var overrides = JSON.parse(instance.value);\r\n overrides.map((value) => {\r\n value.url = M.cfg.wwwroot+'/mod/pulse/automation/instances/edit.php?instanceid='+value.id+'&sesskey='+M.cfg.sesskey\r\n return value;\r\n })\r\n Modal.create({\r\n title: Str.get_string('instanceoverrides', 'pulse'),\r\n body: Template.render('mod_pulse/overrides', {instances: overrides})\r\n }).then((modal) => {\r\n modal.show();\r\n });\r\n }\r\n })\r\n })\r\n };\r\n\r\n const enableTitleOnSubmit = function() {\r\n if (document.forms['pulse-automation-template'] === null) {\r\n return;\r\n }\r\n document.forms['pulse-automation-template'].onsubmit = (e) => document.querySelector('[name=\"title\"]').removeAttribute(\"disabled\");\r\n }\r\n\r\n return {\r\n\r\n init: function() {\r\n returnToFailedTab();\r\n overrideModal();\r\n moveOverRidePosition();\r\n enableTitleOnSubmit();\r\n },\r\n\r\n instanceMenuLink: function() {\r\n var primaryNav = document.querySelector('.secondary-navigation ul.more-nav');\r\n moveOutMoreMenu(primaryNav);\r\n },\r\n\r\n }\r\n})\r\n"],"names":["define","$","Modal","Template","Str","moveOverRidePosition","group","document","querySelectorAll","forEach","overElement","id","replace","element","querySelector","parent","parentNode","innerHTML","nodeToMove","createElement","classList","add","append","observer","MutationObserver","mutations","mutation","console","log","target","overrideElement","disconnect","observe","attributes","childList","subtree","updateAutoCompletionPositions","init","forms","onsubmit","e","invalidElement","hrefSelector","click","returnToFailedTab","templateReference","instanceReference","remove","prepend","trigger","elem","nextSibling","addEventListener","preventDefault","data","dataset","instance","overrides","JSON","parse","value","map","url","M","cfg","wwwroot","sesskey","create","title","get_string","body","render","instances","then","modal","show","overrideModal","removeAttribute","instanceMenuLink","navMenu","menu","forceintomoremenu","removeChild","insertBefore","children","window","dispatchEvent","Event","moveOutMoreMenu"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/completion.min.js b/amd/build/completion.min.js index 865f594..b9781f2 100644 --- a/amd/build/completion.min.js +++ b/amd/build/completion.min.js @@ -1,2 +1,12 @@ -define ("mod_pulse/completion",["core/fragment"],function(a){return{updatecompletionbuttons:function updatecompletionbuttons(){for(var b=document.getElementsByClassName("modtype_pulse"),c=[],d,e=0;e0){var approvebtn,element,referenceNode,completioncontent;Fragment.loadFragment("mod_pulse","completionbuttons",1,params).then((data=>{for(var k in data=JSON.parse(data))approvebtn=data[k],element=document.getElementById("module-"+k),referenceNode=element.getElementsByClassName("contentwithoutlink")[0],(completioncontent=document.createElement("div")).innerHTML=approvebtn,completioncontent.classList.add("pulse-completion-btn"),referenceNode.parentNode.insertBefore(completioncontent,referenceNode.nextSibling);return!0})).fail()}},init:function(){document.body.classList.contains("path-course-view")&&this.updatecompletionbuttons()}}})); + +//# sourceMappingURL=completion.min.js.map \ No newline at end of file diff --git a/amd/build/completion.min.js.map b/amd/build/completion.min.js.map index 046c582..774cc0c 100644 --- a/amd/build/completion.min.js.map +++ b/amd/build/completion.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/completion.js"],"names":["define","Fragment","updatecompletionbuttons","instances","document","getElementsByClassName","modules","moduleid","i","length","instance","id","getAttribute","parseInt","replace","push","params","JSON","stringify","completionbuttons","loadFragment","approvebtn","element","referenceNode","completioncontent","then","data","parse","k","getElementById","createElement","innerHTML","classList","add","parentNode","insertBefore","nextSibling","fail","init","body","contains"],"mappings":"AAyBAA,OAAM,wBAAC,CAAC,eAAD,CAAD,CAAoB,SAASC,CAAT,CAAmB,CAEzC,MAAO,CAMHC,uBAAuB,CAAE,kCAAW,CAGhC,OAFIC,CAAAA,CAAS,CAAGC,QAAQ,CAACC,sBAAT,CAAgC,eAAhC,CAEhB,CADIC,CAAO,CAAG,EACd,CADsBC,CACtB,CAASC,CAAC,CAAG,CAAb,CAAgBA,CAAC,CAAGL,CAAS,CAACM,MAA9B,CAAsCD,CAAC,EAAvC,CAA2C,IACnCE,CAAAA,CAAQ,CAAGP,CAAS,CAACK,CAAD,CADe,CAEnCG,CAAE,CAAGD,CAAQ,CAACE,YAAT,CAAsB,IAAtB,CAF8B,CAGvCL,CAAQ,CAAGM,QAAQ,CAACF,CAAE,CAACG,OAAH,CAAW,SAAX,CAAsB,EAAtB,CAAD,CAAnB,CACAR,CAAO,CAACS,IAAR,CAAaR,CAAb,CACH,CACD,GAAIS,CAAAA,CAAM,CAAG,CAACV,OAAO,CAAGW,IAAI,CAACC,SAAL,CAAeZ,CAAf,CAAX,CAAb,CACA,GAAqB,CAAjB,CAAAA,CAAO,CAACG,MAAZ,CAAwB,IAChBU,CAAAA,CAAiB,CAAGlB,CAAQ,CAACmB,YAAT,CAAsB,WAAtB,CAAmC,mBAAnC,CAAwD,CAAxD,CAA2DJ,CAA3D,CADJ,CAEhBK,CAFgB,CAEJC,CAFI,CAEKC,CAFL,CAEoBC,CAFpB,CAGpBL,CAAiB,CAACM,IAAlB,CAAuB,SAACC,CAAD,CAAU,CAC7BA,CAAI,CAAGT,IAAI,CAACU,KAAL,CAAWD,CAAX,CAAP,CACA,IAAK,GAAIE,CAAAA,CAAT,GAAcF,CAAAA,CAAd,CAAoB,CAChBL,CAAU,CAAGK,CAAI,CAACE,CAAD,CAAjB,CACAN,CAAO,CAAGlB,QAAQ,CAACyB,cAAT,CAAwB,UAAYD,CAApC,CAAV,CACAL,CAAa,CAAGD,CAAO,CAACjB,sBAAR,CAA+B,oBAA/B,EAAqD,CAArD,CAAhB,CACAmB,CAAiB,CAAGpB,QAAQ,CAAC0B,aAAT,CAAuB,KAAvB,CAApB,CACAN,CAAiB,CAACO,SAAlB,CAA8BV,CAA9B,CACAG,CAAiB,CAACQ,SAAlB,CAA4BC,GAA5B,CAAgC,sBAAhC,EACAV,CAAa,CAACW,UAAd,CAAyBC,YAAzB,CAAsCX,CAAtC,CAAyDD,CAAa,CAACa,WAAvE,CACH,CACD,QACH,CAZD,EAYGC,IAZH,EAaH,CACJ,CAjCE,CAsCHC,IAAI,CAAE,eAAW,CACb,GAAIlC,QAAQ,CAACmC,IAAT,CAAcP,SAAd,CAAwBQ,QAAxB,CAAiC,kBAAjC,CAAJ,CAA0D,CACtD,KAAKtC,uBAAL,EACH,CACJ,CA1CE,CA6CV,CA/CK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module javascript to place the placeholders.\n * Modified version of IOMAD Email template emailvars.\n *\n * @package mod_pulse\n * @category Classes - autoloading\n * @copyright 2021, bdecent gmbh bdecent.de\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/fragment'], function(Fragment) {\n\n return {\n\n /**\n * Update completion buttons for each activity based on user role.\n * This will load the template using fragment.\n */\n updatecompletionbuttons: function() {\n var instances = document.getElementsByClassName('modtype_pulse');\n var modules = []; var moduleid;\n for (var i = 0; i < instances.length; i++) {\n var instance = instances[i];\n var id = instance.getAttribute('id');\n moduleid = parseInt(id.replace('module-', ''));\n modules.push(moduleid);\n }\n var params = {modules: JSON.stringify(modules)};\n if (modules.length > 0) {\n let completionbuttons = Fragment.loadFragment('mod_pulse', 'completionbuttons', 1, params);\n var approvebtn, element, referenceNode, completioncontent;\n completionbuttons.then((data) => {\n data = JSON.parse(data);\n for (var k in data) {\n approvebtn = data[k];\n element = document.getElementById('module-' + k);\n referenceNode = element.getElementsByClassName('contentwithoutlink')[0];\n completioncontent = document.createElement('div');\n completioncontent.innerHTML = approvebtn;\n completioncontent.classList.add('pulse-completion-btn');\n referenceNode.parentNode.insertBefore(completioncontent, referenceNode.nextSibling);\n }\n return true;\n }).fail();\n }\n },\n\n /**\n * If the page is course view page then call the completion buttons to insert.\n */\n init: function() {\n if (document.body.classList.contains('path-course-view')) {\n this.updatecompletionbuttons();\n }\n },\n\n };\n});\n"],"file":"completion.min.js"} \ No newline at end of file +{"version":3,"file":"completion.min.js","sources":["../src/completion.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Module javascript to place the placeholders.\r\n * Modified version of IOMAD Email template emailvars.\r\n *\r\n * @module mod_pulse/completion\r\n * @category Classes - autoloading\r\n * @copyright 2021, bdecent gmbh bdecent.de\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine(['core/fragment'], function(Fragment) {\r\n\r\n return {\r\n\r\n /**\r\n * Update completion buttons for each activity based on user role.\r\n * This will load the template using fragment.\r\n */\r\n updatecompletionbuttons: function() {\r\n var instances = document.getElementsByClassName('modtype_pulse');\r\n var modules = []; var moduleid;\r\n for (var i = 0; i < instances.length; i++) {\r\n var instance = instances[i];\r\n var id = instance.getAttribute('id');\r\n moduleid = parseInt(id.replace('module-', ''));\r\n modules.push(moduleid);\r\n }\r\n var params = {modules: JSON.stringify(modules)};\r\n if (modules.length > 0) {\r\n let completionbuttons = Fragment.loadFragment('mod_pulse', 'completionbuttons', 1, params);\r\n var approvebtn, element, referenceNode, completioncontent;\r\n completionbuttons.then((data) => {\r\n data = JSON.parse(data);\r\n for (var k in data) {\r\n approvebtn = data[k];\r\n element = document.getElementById('module-' + k);\r\n referenceNode = element.getElementsByClassName('contentwithoutlink')[0];\r\n completioncontent = document.createElement('div');\r\n completioncontent.innerHTML = approvebtn;\r\n completioncontent.classList.add('pulse-completion-btn');\r\n referenceNode.parentNode.insertBefore(completioncontent, referenceNode.nextSibling);\r\n }\r\n return true;\r\n }).fail();\r\n }\r\n },\r\n\r\n /**\r\n * If the page is course view page then call the completion buttons to insert.\r\n */\r\n init: function() {\r\n if (document.body.classList.contains('path-course-view')) {\r\n this.updatecompletionbuttons();\r\n }\r\n },\r\n\r\n };\r\n});\r\n"],"names":["define","Fragment","updatecompletionbuttons","moduleid","instances","document","getElementsByClassName","modules","i","length","id","getAttribute","parseInt","replace","push","params","JSON","stringify","approvebtn","element","referenceNode","completioncontent","loadFragment","then","data","k","parse","getElementById","createElement","innerHTML","classList","add","parentNode","insertBefore","nextSibling","fail","init","body","contains"],"mappings":";;;;;;;;;AAyBAA,8BAAO,CAAC,kBAAkB,SAASC,gBAExB,CAMHC,wBAAyB,mBAECC,SADlBC,UAAYC,SAASC,uBAAuB,iBAC5CC,QAAU,GACLC,EAAI,EAAGA,EAAIJ,UAAUK,OAAQD,IAAK,KAEnCE,GADWN,UAAUI,GACPG,aAAa,MAC/BR,SAAWS,SAASF,GAAGG,QAAQ,UAAW,KAC1CN,QAAQO,KAAKX,cAEbY,OAAS,CAACR,QAAUS,KAAKC,UAAUV,aACnCA,QAAQE,OAAS,EAAG,KAEhBS,WAAYC,QAASC,cAAeC,kBADhBpB,SAASqB,aAAa,YAAa,oBAAqB,EAAGP,QAEjEQ,MAAMC,WAEf,IAAIC,KADTD,KAAOR,KAAKU,MAAMF,MAEdN,WAAaM,KAAKC,GAClBN,QAAUd,SAASsB,eAAe,UAAYF,GAC9CL,cAAgBD,QAAQb,uBAAuB,sBAAsB,IACrEe,kBAAoBhB,SAASuB,cAAc,QACzBC,UAAYX,WAC9BG,kBAAkBS,UAAUC,IAAI,wBAChCX,cAAcY,WAAWC,aAAaZ,kBAAmBD,cAAcc,oBAEpE,KACRC,SAOXC,KAAM,WACE/B,SAASgC,KAAKP,UAAUQ,SAAS,0BAC5BpC"} \ No newline at end of file diff --git a/amd/build/events.min.js b/amd/build/events.min.js new file mode 100644 index 0000000..0ec9fc9 --- /dev/null +++ b/amd/build/events.min.js @@ -0,0 +1,11 @@ +/** + * Contain the events the data privacy tool can fire. + * + * @module mod_pulse/events + * @category Classes - autoloading + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("mod_pulse/events",[],(function(){return{save:"mod_pulse-preset:save",customize:"mod_pulse-preset:customize"}})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/amd/build/events.min.js.map b/amd/build/events.min.js.map new file mode 100644 index 0000000..0073203 --- /dev/null +++ b/amd/build/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Contain the events the data privacy tool can fire.\r\n *\r\n * @module mod_pulse/events\r\n * @category Classes - autoloading\r\n * @copyright 2021, bdecent gmbh bdecent.de\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\n define([], function() {\r\n return {\r\n save: 'mod_pulse-preset:save',\r\n customize: 'mod_pulse-preset:customize',\r\n };\r\n});\r\n"],"names":["define","save","customize"],"mappings":";;;;;;;;AAwBCA,0BAAO,IAAI,iBACD,CACHC,KAAM,wBACNC,UAAW"} \ No newline at end of file diff --git a/amd/build/modal_preset.min.js b/amd/build/modal_preset.min.js new file mode 100644 index 0000000..bd9ceb3 --- /dev/null +++ b/amd/build/modal_preset.min.js @@ -0,0 +1,3 @@ +define("mod_pulse/modal_preset",["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","mod_pulse/events"],(function($,Notification,CustomEvents,Modal,ModalRegistry,PresetEvents){var registered=!1,SELECTORS_SAVE_BUTTON='[data-action="save"]',SELECTORS_CUSTOMIZE_BUTTON='[data-action="customize"]',SELECTORS_CANCEL_BUTTON='[data-action="cancel"]',ModalPreset=function(root){Modal.call(this,root),this.getFooter().find(SELECTORS_SAVE_BUTTON).length||Notification.exception({message:'No "Apply and save" button found'}),this.getFooter().find(SELECTORS_CUSTOMIZE_BUTTON).length||Notification.exception({message:'No "Apply and customize" button found'}),this.getFooter().find(SELECTORS_CANCEL_BUTTON).length||Notification.exception({message:"No cancel button found"})};return ModalPreset.TYPE="PresetModal",(ModalPreset.prototype=Object.create(Modal.prototype)).constructor=ModalPreset,ModalPreset.prototype.formData="",ModalPreset.prototype.registerEventListeners=function(){Modal.prototype.registerEventListeners.call(this),this.getModal().on(CustomEvents.events.activate,SELECTORS_SAVE_BUTTON,function(event,data){document.querySelectorAll(".preset-config-params form.mform").forEach((form=>{form.importmethod.value="save",form.addEventListener("submit",(function(e){e.preventDefault()}))})),0!=document.querySelectorAll('.preset-config-params [data-fieldtype="submit"] input').length&&document.querySelectorAll('.preset-config-params [data-fieldtype="submit"] input')[0].click();var approveEvent=$.Event(PresetEvents.save);this.getRoot().trigger(approveEvent,this),approveEvent.isDefaultPrevented()||(this.destroy(),data.originalEvent.preventDefault()),event.preventDefault()}.bind(this)),this.getModal().on(CustomEvents.events.activate,SELECTORS_CUSTOMIZE_BUTTON,function(event,data){document.querySelectorAll(".preset-config-params form.mform").forEach((form=>{form.importmethod.value="customize"}));var customizeEvent=$.Event(PresetEvents.customize);this.getRoot().trigger(customizeEvent,this),customizeEvent.isDefaultPrevented()||data.originalEvent.preventDefault(),event.preventDefault()}.bind(this)),this.getModal().on(CustomEvents.events.activate,SELECTORS_CANCEL_BUTTON,function(){this.destroy()}.bind(this))},registered||(ModalRegistry.register(ModalPreset.TYPE,ModalPreset,"mod_pulse/modal_preset"),registered=!0),ModalPreset})); + +//# sourceMappingURL=modal_preset.min.js.map \ No newline at end of file diff --git a/amd/build/modal_preset.min.js.map b/amd/build/modal_preset.min.js.map new file mode 100644 index 0000000..d27113b --- /dev/null +++ b/amd/build/modal_preset.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"modal_preset.min.js","sources":["../src/modal_preset.js"],"sourcesContent":["define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry', 'mod_pulse/events'],\r\n function($, Notification, CustomEvents, Modal, ModalRegistry, PresetEvents) {\r\n\r\n var registered = false;\r\n var SELECTORS = {\r\n SAVE_BUTTON: '[data-action=\"save\"]',\r\n CUSTOMIZE_BUTTON: '[data-action=\"customize\"]',\r\n CANCEL_BUTTON: '[data-action=\"cancel\"]',\r\n };\r\n\r\n /**\r\n * Constructor for the Modal.\r\n *\r\n * @param {object} root The root jQuery element for the modal\r\n */\r\n var ModalPreset = function(root) {\r\n Modal.call(this, root);\r\n\r\n if (!this.getFooter().find(SELECTORS.SAVE_BUTTON).length) {\r\n Notification.exception({message: 'No \"Apply and save\" button found'});\r\n }\r\n\r\n if (!this.getFooter().find(SELECTORS.CUSTOMIZE_BUTTON).length) {\r\n Notification.exception({message: 'No \"Apply and customize\" button found'});\r\n }\r\n\r\n if (!this.getFooter().find(SELECTORS.CANCEL_BUTTON).length) {\r\n Notification.exception({message: 'No cancel button found'});\r\n }\r\n };\r\n\r\n ModalPreset.TYPE = 'PresetModal';\r\n ModalPreset.prototype = Object.create(Modal.prototype);\r\n ModalPreset.prototype.constructor = ModalPreset;\r\n ModalPreset.prototype.formData = '';\r\n\r\n /**\r\n * Set up all of the event handling for the modal.\r\n *\r\n * @method registerEventListeners\r\n */\r\n ModalPreset.prototype.registerEventListeners = function() {\r\n // Apply parent event listeners.\r\n Modal.prototype.registerEventListeners.call(this);\r\n\r\n this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(event, data) {\r\n // Load the backupfile.\r\n document.querySelectorAll('.preset-config-params form.mform').forEach(form => {\r\n form.importmethod.value = 'save';\r\n form.addEventListener('submit', function(e) {\r\n e.preventDefault();\r\n });\r\n });\r\n\r\n if (document.querySelectorAll('.preset-config-params [data-fieldtype=\"submit\"] input').length != 0) {\r\n document.querySelectorAll('.preset-config-params [data-fieldtype=\"submit\"] input')[0].click();\r\n }\r\n\r\n var approveEvent = $.Event(PresetEvents.save);\r\n this.getRoot().trigger(approveEvent, this);\r\n\r\n if (!approveEvent.isDefaultPrevented()) {\r\n this.destroy();\r\n data.originalEvent.preventDefault();\r\n }\r\n event.preventDefault();\r\n }.bind(this));\r\n\r\n\r\n this.getModal().on(CustomEvents.events.activate, SELECTORS.CUSTOMIZE_BUTTON, function(event, data) {\r\n // Add your logic for when the login button is clicked. This could include the form validation,\r\n document.querySelectorAll('.preset-config-params form.mform').forEach(form => {\r\n form.importmethod.value = 'customize';\r\n });\r\n\r\n var customizeEvent = $.Event(PresetEvents.customize);\r\n this.getRoot().trigger(customizeEvent, this);\r\n\r\n if (!customizeEvent.isDefaultPrevented()) {\r\n data.originalEvent.preventDefault();\r\n }\r\n event.preventDefault();\r\n\r\n }.bind(this));\r\n\r\n this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function() {\r\n this.destroy();\r\n }.bind(this));\r\n };\r\n\r\n // Automatically register with the modal registry the first time this module is imported so that you can create modals\r\n // of this type using the modal factory.\r\n if (!registered) {\r\n ModalRegistry.register(ModalPreset.TYPE, ModalPreset, 'mod_pulse/modal_preset');\r\n registered = true;\r\n }\r\n\r\n return ModalPreset;\r\n});\r\n"],"names":["define","$","Notification","CustomEvents","Modal","ModalRegistry","PresetEvents","registered","SELECTORS","ModalPreset","root","call","this","getFooter","find","length","exception","message","TYPE","prototype","Object","create","constructor","formData","registerEventListeners","getModal","on","events","activate","event","data","document","querySelectorAll","forEach","form","importmethod","value","addEventListener","e","preventDefault","click","approveEvent","Event","save","getRoot","trigger","isDefaultPrevented","destroy","originalEvent","bind","customizeEvent","customize","register"],"mappings":"AAAAA,gCAAO,CAAC,SAAU,oBAAqB,iCAAkC,aAAc,sBAAuB,qBACtG,SAASC,EAAGC,aAAcC,aAAcC,MAAOC,cAAeC,kBAE9DC,YAAa,EACbC,sBACa,uBADbA,2BAEkB,4BAFlBA,wBAGe,yBAQfC,YAAc,SAASC,MACvBN,MAAMO,KAAKC,KAAMF,MAEZE,KAAKC,YAAYC,KAAKN,uBAAuBO,QAC9Cb,aAAac,UAAU,CAACC,QAAS,qCAGhCL,KAAKC,YAAYC,KAAKN,4BAA4BO,QACnDb,aAAac,UAAU,CAACC,QAAS,0CAGhCL,KAAKC,YAAYC,KAAKN,yBAAyBO,QAChDb,aAAac,UAAU,CAACC,QAAS,mCAIzCR,YAAYS,KAAO,eACnBT,YAAYU,UAAYC,OAAOC,OAAOjB,MAAMe,YACtBG,YAAcb,YACpCA,YAAYU,UAAUI,SAAW,GAOjCd,YAAYU,UAAUK,uBAAyB,WAE3CpB,MAAMe,UAAUK,uBAAuBb,KAAKC,WAEvCa,WAAWC,GAAGvB,aAAawB,OAAOC,SAAUpB,sBAAuB,SAASqB,MAAOC,MAEpFC,SAASC,iBAAiB,oCAAoCC,SAAQC,OAClEA,KAAKC,aAAaC,MAAQ,OAC1BF,KAAKG,iBAAiB,UAAU,SAASC,GACrCA,EAAEC,uBAIuF,GAA7FR,SAASC,iBAAiB,yDAAyDjB,QACnFgB,SAASC,iBAAiB,yDAAyD,GAAGQ,YAGtFC,aAAexC,EAAEyC,MAAMpC,aAAaqC,WACnCC,UAAUC,QAAQJ,aAAc7B,MAEhC6B,aAAaK,4BACTC,UACLjB,KAAKkB,cAAcT,kBAEvBV,MAAMU,kBACRU,KAAKrC,YAGFa,WAAWC,GAAGvB,aAAawB,OAAOC,SAAUpB,2BAA4B,SAASqB,MAAOC,MAEzFC,SAASC,iBAAiB,oCAAoCC,SAAQC,OAClEA,KAAKC,aAAaC,MAAQ,mBAG1Bc,eAAiBjD,EAAEyC,MAAMpC,aAAa6C,gBACrCP,UAAUC,QAAQK,eAAgBtC,MAElCsC,eAAeJ,sBAChBhB,KAAKkB,cAAcT,iBAEvBV,MAAMU,kBAERU,KAAKrC,YAEFa,WAAWC,GAAGvB,aAAawB,OAAOC,SAAUpB,wBAAyB,gBACjEuC,WACPE,KAAKrC,QAKNL,aACDF,cAAc+C,SAAS3C,YAAYS,KAAMT,YAAa,0BACtDF,YAAa,GAGVE"} \ No newline at end of file diff --git a/amd/build/module.min.js b/amd/build/module.min.js index a9891ea..abd48fa 100644 --- a/amd/build/module.min.js +++ b/amd/build/module.min.js @@ -1,2 +1,12 @@ -define ("mod_pulse/module",[],function(){return{init:function init(){for(var a=this,b=document.getElementsByClassName("fitem_id_templatevars_editor"),c=0;c{elem.addEventListener("click",(function(e){var EditorInput=e.currentTarget.querySelector('[id*="_editoreditable"]');module.insertCaretActive(EditorInput)}))}));var targetNode=document.querySelector("textarea[id$=_editor]");null!==targetNode&&new MutationObserver((function(){"none"==targetNode.style.display&&setTimeout(initIframeListeners,100)})).observe(targetNode,{attributes:!0,childList:!0});const initIframeListeners=()=>{var iframes=document.querySelectorAll('[data-fieldtype="editor"] iframe');if(null===iframes||!iframes.length)return!1;iframes.forEach((iframe=>{iframe.contentDocument.addEventListener("click",(function(e){var currentFrame=e.target;iframes.forEach((frame=>{var frameElem=frame.contentDocument.querySelector(".insertatcaretactive");null!=frameElem&&frameElem.classList.remove("insertatcaretactive")}));var contentBody=currentFrame.querySelector("body");if(null!==contentBody){contentBody.classList.add("insertatcaretactive");var id=contentBody.dataset.id,editor=window.tinyMCE.get(id);tinyEditor=editor}}))}))};for(var clickforword=document.getElementsByClassName("clickforword"),i=0;i{const span=document.createElement("span");span.classList="badge badge-info pulse-completion-roles",node.after(span),span.appendChild(node)}))}},insertCaretActive:function(EditorInput){if(null!==EditorInput){tinyEditor=!1;for(var caret=document.getElementsByClassName("insertatcaretactive"),j=0;j3===node.nodeType&&node.textContent.trim().length>1))},isSelectionInsideDiv:div=>{const selection=window.getSelection();if(0===selection.rangeCount)return!1;const startNode=selection.getRangeAt(0).startContainer,endNode=selection.getRangeAt(0).endContainer;return div.contains(startNode)&&div.contains(endNode)},insertAtCaret:function(myValue){for(var sel,range,caretelements=document.getElementsByClassName("insertatcaretactive"),n=0;n.\n\n/**\n * Module javascript to place the placeholders.\n * Modified version of IOMAD Email template emailvars.\n *\n * @package mod_pulse\n * @category Classes - autoloading\n * @copyright 2021, bdecent gmbh bdecent.de\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([], function() {\n\n return {\n\n /**\n * Setup the classes to editors works with placeholders.\n */\n init: function() {\n var module = this;\n var templatevars = document.getElementsByClassName(\"fitem_id_templatevars_editor\");\n for (var l = 0; l < templatevars.length; l++) {\n templatevars[l].addEventListener('click', function() {\n var EditorInput = document.getElementById('id_pulse_content_editoreditable');\n var caret = document.getElementsByClassName(\"insertatcaretactive\");\n for (var j = 0; j < caret.length; j++) {\n caret[j].classList.remove(\"insertatcaretactive\");\n }\n EditorInput.classList.add(\"insertatcaretactive\");\n\n });\n }\n var clickforword = document.getElementsByClassName('clickforword');\n for (var i = 0; i < clickforword.length; i++) {\n clickforword[i].addEventListener('click', function(e) {\n e.preventDefault(); // To prevent the default behaviour of a tag.\n module.insertAtCaret(\"{\" + this.getAttribute('data-text') + \"}\");\n });\n }\n\n // Make selected roles as badges in module edit form page.\n if (document.getElementById('page-mod-pulse-mod')\n .querySelector(\"#fgroup_id_completionrequireapproval [data-fieldtype='autocomplete']\") !== null) {\n const textNodes = this.getAllTextNodes(\n document.getElementById('page-mod-pulse-mod')\n .querySelector(\"#fgroup_id_completionrequireapproval [data-fieldtype='autocomplete']\")\n );\n textNodes.forEach(node => {\n const span = document.createElement('span');\n span.classList = 'badge badge-info pulse-completion-roles';\n node.after(span);\n span.appendChild(node);\n });\n }\n },\n\n /**\n * Filter text from node.\n * @param {string} element\n * @returns {array} list of childNodes.\n */\n getAllTextNodes: function(element) {\n return Array.from(element.childNodes)\n .filter(node => node.nodeType === 3 && node.textContent.trim().length > 1);\n },\n\n /**\n * Insert the placeholder in selected caret place.\n * @param {string} myValue\n */\n insertAtCaret: function(myValue) {\n var caretelements = document.getElementsByClassName(\"insertatcaretactive\");\n var sel, range;\n for (var n = 0; n < caretelements.length; n++) {\n var thiselem = caretelements[n];\n\n if (typeof thiselem.value === 'undefined' && window.getSelection) {\n sel = window.getSelection();\n if (sel.getRangeAt && sel.rangeCount) {\n range = sel.getRangeAt(0);\n range.deleteContents();\n range.insertNode(document.createTextNode(myValue));\n\n for (let position = 0; position != (myValue.length + 1); position++) {\n sel.modify(\"move\", \"right\", \"character\");\n }\n }\n } else if (typeof thiselem.value === 'undefined' && document.selection && document.selection.createRange) {\n range = document.selection.createRange();\n range.text = myValue;\n }\n\n if (typeof thiselem.value !== 'undefined') {\n if (document.selection) {\n // For browsers like Internet Explorer.\n thiselem.focus();\n sel = document.selection.createRange();\n sel.text = myValue;\n thiselem.focus();\n } else if (thiselem.selectionStart || thiselem.selectionStart == '0') {\n // For browsers like Firefox and Webkit based.\n var startPos = thiselem.selectionStart;\n var endPos = thiselem.selectionEnd;\n thiselem.value = thiselem.value.substring(0, startPos)\n + myValue + thiselem.value.substring(endPos, thiselem.value.length);\n thiselem.focus();\n thiselem.selectionStart = startPos + myValue.length;\n thiselem.selectionEnd = startPos + myValue.length;\n thiselem.focus();\n } else {\n thiselem.value += myValue;\n thiselem.focus();\n }\n }\n }\n },\n };\n});\n"],"file":"module.min.js"} \ No newline at end of file +{"version":3,"file":"module.min.js","sources":["../src/module.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Module javascript to place the placeholders.\r\n * Modified version of IOMAD Email template emailvars.\r\n *\r\n * @module mod_pulse/module\r\n * @category Classes - autoloading\r\n * @copyright 2021, bdecent gmbh bdecent.de\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine(['core_editor/events'], function(events) {\r\n\r\n var tinyEditor = false;\r\n\r\n return {\r\n /**\r\n * Setup the classes to editors works with placeholders.\r\n */\r\n init: function() {\r\n var module = this;\r\n\r\n var templatevars = document.getElementsByClassName(\"fitem_id_templatevars_editor\");\r\n for (var l = 0; l < templatevars.length; l++) {\r\n templatevars[l].addEventListener('click', function() {\r\n var EditorInput = document.getElementById('id_pulse_content_editoreditable');\r\n if (EditorInput !== null) {\r\n module.insertCaretActive(EditorInput);\r\n }\r\n });\r\n }\r\n\r\n var notificationheader = document.getElementById('admin-notificationheader');\r\n if (notificationheader !== null) {\r\n notificationheader.addEventListener('click', function() {\r\n var EditorInput = document.getElementById('id_s_mod_pulse_notificationheadereditable');\r\n module.insertCaretActive(EditorInput);\r\n });\r\n }\r\n\r\n var notificationfooter = document.getElementById('admin-notificationfooter');\r\n if (notificationfooter !== null) {\r\n notificationfooter.addEventListener('click', function() {\r\n var EditorInput = document.getElementById('id_s_mod_pulse_notificationfootereditable');\r\n module.insertCaretActive(EditorInput);\r\n });\r\n }\r\n\r\n var templatevars = document.getElementsByClassName(\"fitem_id_templatevars_editor\");\r\n if (templatevars) {\r\n templatevars.forEach((elem) => {\r\n elem.addEventListener('click', function(e) {\r\n var target = e.currentTarget;\r\n var EditorInput = target.querySelector('[id*=\"_editoreditable\"]');\r\n module.insertCaretActive(EditorInput);\r\n });\r\n })\r\n }\r\n\r\n // console.log(window.tinyMCE.get());\r\n var targetNode = document.querySelector('textarea[id$=_editor]');\r\n if (targetNode !== null) {\r\n var observer = new MutationObserver(function() {\r\n if (targetNode.style.display == 'none') {\r\n setTimeout(initIframeListeners, 100);\r\n }\r\n });\r\n observer.observe(targetNode, { attributes: true, childList: true });\r\n }\r\n\r\n const initIframeListeners = () => {\r\n\r\n var iframes = document.querySelectorAll('[data-fieldtype=\"editor\"] iframe');\r\n if (iframes === null || !iframes.length) {\r\n return false;\r\n }\r\n\r\n iframes.forEach((iframe) => {\r\n iframe.contentDocument.addEventListener('click', function(e) {\r\n\r\n var currentFrame = e.target;\r\n iframes.forEach((frame) => {\r\n var frameElem = frame.contentDocument.querySelector(\".insertatcaretactive\");\r\n if (frameElem != null) {\r\n frameElem.classList.remove(\"insertatcaretactive\");\r\n }\r\n });\r\n\r\n var contentBody = currentFrame.querySelector('body');\r\n if (contentBody !== null) {\r\n contentBody.classList.add(\"insertatcaretactive\");\r\n var id = contentBody.dataset.id;\r\n var editor = window.tinyMCE.get(id);\r\n tinyEditor = editor;\r\n }\r\n });\r\n });\r\n }\r\n\r\n\r\n var clickforword = document.getElementsByClassName('clickforword');\r\n for (var i = 0; i < clickforword.length; i++) {\r\n clickforword[i].addEventListener('click', function(e) {\r\n e.preventDefault(); // To prevent the default behaviour of a tag.\r\n\r\n var content = \"{\" + this.getAttribute('data-text') + \"}\";\r\n if (tinyEditor) {\r\n tinyEditor.selection.setContent(content);\r\n } else {\r\n module.insertAtCaret(content);\r\n }\r\n });\r\n }\r\n\r\n // Make selected roles as badges in module edit form page.\r\n if (document.getElementById('page-mod-pulse-mod') !== null && document.getElementById('page-mod-pulse-mod')\r\n .querySelector(\"#fgroup_id_completionrequireapproval [data-fieldtype='autocomplete']\") !== null) {\r\n const textNodes = this.getAllTextNodes(\r\n document.getElementById('page-mod-pulse-mod')\r\n .querySelector(\"#fgroup_id_completionrequireapproval [data-fieldtype='autocomplete']\")\r\n );\r\n textNodes.forEach(node => {\r\n const span = document.createElement('span');\r\n span.classList = 'badge badge-info pulse-completion-roles';\r\n node.after(span);\r\n span.appendChild(node);\r\n });\r\n }\r\n },\r\n\r\n insertCaretActive: function(EditorInput) {\r\n if (EditorInput === null) {\r\n return;\r\n }\r\n tinyEditor = false;\r\n var caret = document.getElementsByClassName(\"insertatcaretactive\");\r\n for (var j = 0; j < caret.length; j++) {\r\n caret[j].classList.remove(\"insertatcaretactive\");\r\n }\r\n EditorInput.classList.add(\"insertatcaretactive\");\r\n },\r\n\r\n /**\r\n * Filter text from node.\r\n * @param {string} element\r\n * @returns {array} list of childNodes.\r\n */\r\n getAllTextNodes: function(element) {\r\n return Array.from(element.childNodes)\r\n .filter(node => node.nodeType === 3 && node.textContent.trim().length > 1);\r\n },\r\n\r\n /**\r\n * Find the selection is inside the editor\r\n *\r\n * @param {string} div\r\n * @returns\r\n */\r\n isSelectionInsideDiv: (div) => {\r\n const selection = window.getSelection();\r\n if (selection.rangeCount === 0) {\r\n return false;\r\n }\r\n\r\n // Get the start and end nodes of the selection.\r\n const startNode = selection.getRangeAt(0).startContainer;\r\n const endNode = selection.getRangeAt(0).endContainer;\r\n\r\n // Check if the start and end nodes are both descendants of the editor div.\r\n return div.contains(startNode) && div.contains(endNode);\r\n },\r\n\r\n /**\r\n * Insert the placeholder in selected caret place.\r\n * @param {string} myValue\r\n */\r\n insertAtCaret: function(myValue) {\r\n var caretelements = document.getElementsByClassName(\"insertatcaretactive\");\r\n var sel, range;\r\n for (var n = 0; n < caretelements.length; n++) {\r\n var thiselem = caretelements[n];\r\n\r\n if (typeof thiselem.value === 'undefined' && window.getSelection && this.isSelectionInsideDiv(thiselem)) {\r\n sel = window.getSelection();\r\n if (sel.getRangeAt && sel.rangeCount) {\r\n range = sel.getRangeAt(0);\r\n range.deleteContents();\r\n range.insertNode(document.createTextNode(myValue));\r\n\r\n for (let position = 0; position != (myValue.length + 1); position++) {\r\n sel.modify(\"move\", \"right\", \"character\");\r\n }\r\n }\r\n } else if (typeof thiselem.value === 'undefined' && document.selection && document.selection.createRange) {\r\n range = document.selection.createRange();\r\n range.text = myValue;\r\n }\r\n\r\n if (typeof thiselem.value !== 'undefined') {\r\n if (document.selection) {\r\n // For browsers like Internet Explorer.\r\n thiselem.focus();\r\n sel = document.selection.createRange();\r\n sel.text = myValue;\r\n thiselem.focus();\r\n } else if (thiselem.selectionStart || thiselem.selectionStart == '0') {\r\n // For browsers like Firefox and Webkit based.\r\n var startPos = thiselem.selectionStart;\r\n var endPos = thiselem.selectionEnd;\r\n thiselem.value = thiselem.value.substring(0, startPos)\r\n + myValue + thiselem.value.substring(endPos, thiselem.value.length);\r\n thiselem.focus();\r\n thiselem.selectionStart = startPos + myValue.length;\r\n thiselem.selectionEnd = startPos + myValue.length;\r\n thiselem.focus();\r\n } else {\r\n thiselem.value += myValue;\r\n thiselem.focus();\r\n }\r\n }\r\n }\r\n },\r\n };\r\n});\r\n"],"names":["define","events","tinyEditor","init","module","this","templatevars","document","getElementsByClassName","l","length","addEventListener","EditorInput","getElementById","insertCaretActive","notificationheader","notificationfooter","forEach","elem","e","currentTarget","querySelector","targetNode","MutationObserver","style","display","setTimeout","initIframeListeners","observe","attributes","childList","iframes","querySelectorAll","iframe","contentDocument","currentFrame","target","frame","frameElem","classList","remove","contentBody","add","id","dataset","editor","window","tinyMCE","get","clickforword","i","preventDefault","content","getAttribute","selection","setContent","insertAtCaret","getAllTextNodes","node","span","createElement","after","appendChild","caret","j","element","Array","from","childNodes","filter","nodeType","textContent","trim","isSelectionInsideDiv","div","getSelection","rangeCount","startNode","getRangeAt","startContainer","endNode","endContainer","contains","myValue","sel","range","caretelements","n","thiselem","value","deleteContents","insertNode","createTextNode","position","modify","createRange","text","focus","selectionStart","startPos","endPos","selectionEnd","substring"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/preset.min.js b/amd/build/preset.min.js new file mode 100644 index 0000000..465fa7a --- /dev/null +++ b/amd/build/preset.min.js @@ -0,0 +1,3 @@ +define("mod_pulse/preset",["jquery","core/modal_factory","mod_pulse/modal_preset","mod_pulse/events","core/str","core/fragment","core/ajax","core/templates","core/loadingicon","core/notification","core/modal_events"],(function($,Modal,ModalPreset,PresetEvents,Str,Fragment,AJAX,Templates,Loadingicon,Notification,ModalEvents){var SELECTOR_presetAvailability=".preset-config-params .availability-field",Preset=function(contextId,courseid,section){this.contextId=contextId,this.courseid=courseid,this.section=section,this.loadPresetsList()};return Preset.prototype.listElement={selector:"pulse-presets-data",loaded:"data-listloaded"},Preset.prototype.contextId=0,Preset.prototype.courseid=0,Preset.prototype.section=0,Preset.prototype.pageparams=[],Preset.prototype.loadIconElement=".modal-footer #loader-icon",Preset.prototype.actionbuttons=".modal-footer button",Preset.prototype.setupmodal=function(){var THIS=this,triggerelement=document.querySelectorAll(".pulse-usepreset"),attachmentPoint=document.createElement("div");attachmentPoint.classList.add("modal-preset"),triggerelement.forEach((element=>element.addEventListener("click",(()=>{var presetid=element.getAttribute("data-presetid"),presettitle=element.getAttribute("data-presettitle"),params={presetid:presetid,courseid:THIS.courseid,section:THIS.section};document.body.prepend(attachmentPoint),Modal.create({type:ModalPreset.TYPE,title:Str.get_string("presetmodaltitle","pulse",{title:presettitle}),body:Fragment.loadFragment("mod_pulse","get_preset_preview",THIS.contextId,params),large:!0}).then((modal=>(modal.attachmentPoint=attachmentPoint,modal.show(),modal.getRoot().on(ModalEvents.bodyRendered,(function(){THIS.reinitAvailability(SELECTOR_presetAvailability),THIS.fieldChangedEvent()})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy.bind(modal),THIS.reinitAvailability()})),modal.getRoot().on(PresetEvents.customize,(()=>{var modform=document.querySelector("#mod-pulse-form"),modformdata=new FormData(modform);modal.getRoot().get(0).querySelectorAll("form").forEach((form=>{var formdata=new FormData(form);formdata=new URLSearchParams(formdata).toString();var pageparams=new URLSearchParams(modformdata).toString();params={formdata:formdata,pageparams:pageparams},Loadingicon.addIconToContainer(this.loadIconElement),THIS.disableButtons(),THIS.applyCustomize(params,THIS.contextId,modal)}))})),modal.getRoot().on(PresetEvents.save,(e=>{e.preventDefault(),Loadingicon.addIconToContainer(this.loadIconElement),THIS.disableButtons();var formdata={};modal.getRoot().get(0).querySelectorAll("form").forEach((form=>{formdata=new FormData(form),this.restorePreset(formdata,THIS.contextId)}))})),!0))).catch(Notification.exception)}))))},Preset.prototype.fieldChangedEvent=()=>{var fieldName,changeinput,id,changeName,split,confParam=document.getElementById("preset-configurable-params"),methods=["fixed","relative"];confParam.querySelectorAll("input, select, textarea").forEach((field=>{field.addEventListener("change",(event=>{fieldName=event.target.getAttribute("name"),null!==confParam.querySelector('input[name="'+fieldName+'_changed"]')&&(confParam.querySelector('input[name="'+fieldName+'_changed"]').value=!0)}))})),["first","second","recurring"].forEach((reminder=>{confParam.querySelectorAll('[name="'+reminder+'_schedule"').forEach((schedule=>{schedule.addEventListener("change",(e=>{changeName=e.target.getAttribute("name"),changeinput='input[name="'+changeName+'_arr_changed"]',confParam.querySelector(changeinput).value=!0}))})),methods.forEach((method=>{id=reminder+"_"+method+"date",confParam.querySelectorAll('[name*="'+id+'"]').forEach((opt=>{opt.addEventListener("change",(e=>{split=e.target.getAttribute("name").split("["),changeName=split.hasOwnProperty(1)?split[0]:split,changeinput='input[name="'+changeName+'_changed"]',confParam.querySelector(changeinput).value=!0}))}))}))}))},Preset.prototype.reinitAvailability=function(){let selector=arguments.length>0&&void 0!==arguments[0]?arguments[0]:".availability-field";void 0!==M.core_availability.form&&(this.resetRestrictPlugins(),document.querySelectorAll(selector).forEach((field=>field.parentNode.removeChild(field))),M.core_availability.form.init())},Preset.prototype.resetRestrictPlugins=function(){if(void 0!==M.core_availability.form&&null!==document.getElementById("id_availabilityconditionsjson")){M.core_availability.form.restrictByGroup=null;var availabilityPlugins=void 0!==M.core_availability.form.plugins?M.core_availability.form.plugins:{},plugin="";for(var i in availabilityPlugins)plugin="availability_"+i,M.hasOwnProperty(plugin)&&(M[plugin].form.addedEvents=!1)}},Preset.prototype.applyCustomize=function(params,contextID,modal){Fragment.loadFragment("mod_pulse","apply_preset",contextID,params).done(((html,js)=>{modal.destroy(),this.resetRestrictPlugins(),this.handleFormSubmissionResponse(html,js)}))},Preset.prototype.disableButtons=function(){var buttons=document.querySelectorAll(this.actionbuttons);for(let $i in buttons)buttons[$i].disabled=!0},Preset.prototype.handleFormSubmissionResponse=(data,js)=>{document.createElement("div").innerHTML=data,Templates.replaceNode('[action="modedit.php"]',data,js)},Preset.prototype.restorePreset=(formdata,contextid)=>{var formdatastr=new URLSearchParams(formdata).toString();AJAX.call([{methodname:"mod_pulse_apply_presets",args:{contextid:contextid,formdata:formdatastr}}])[0].done((response=>{void 0!==(response=JSON.parse(response)).url&&(window.location.href=response.url)}))},Preset.prototype.loadPresetsList=function(){var listParent=document.getElementById(this.listElement.selector);null!==listParent&&"false"==listParent.getAttribute(this.listElement.loaded)&&Fragment.loadFragment("mod_pulse","get_presetslist",this.contextId,{courseid:this.courseid}).done(((html,js)=>{Templates.replaceNodeContents(listParent,html,js),listParent.setAttribute(this.listElement.loaded,"true"),this.setupmodal()}))},{init:(contextId,courseid,section)=>{new Preset(contextId,courseid,section)}}})); + +//# sourceMappingURL=preset.min.js.map \ No newline at end of file diff --git a/amd/build/preset.min.js.map b/amd/build/preset.min.js.map new file mode 100644 index 0000000..43d097f --- /dev/null +++ b/amd/build/preset.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"preset.min.js","sources":["../src/preset.js"],"sourcesContent":["define(['jquery', 'core/modal_factory', 'mod_pulse/modal_preset', 'mod_pulse/events', 'core/str',\r\n'core/fragment', 'core/ajax', 'core/templates', 'core/loadingicon', 'core/notification', 'core/modal_events'],\r\n function($, Modal, ModalPreset, PresetEvents, Str, Fragment, AJAX, Templates, Loadingicon, Notification, ModalEvents) {\r\n\r\n var SELECTOR = {\r\n presetAvailability: '.preset-config-params .availability-field'\r\n };\r\n\r\n /**\r\n * Preset module declaration. Setup the global values.\r\n * @param {int} contextId\r\n * @param {int} courseid\r\n * @param {int} section\r\n */\r\n var Preset = function(contextId, courseid, section) {\r\n this.contextId = contextId;\r\n this.courseid = courseid;\r\n this.section = section;\r\n this.loadPresetsList();\r\n };\r\n\r\n Preset.prototype.listElement = {'selector': 'pulse-presets-data', \"loaded\": \"data-listloaded\"};\r\n\r\n Preset.prototype.contextId = 0;\r\n\r\n Preset.prototype.courseid = 0;\r\n\r\n Preset.prototype.section = 0;\r\n\r\n Preset.prototype.pageparams = [];\r\n\r\n Preset.prototype.loadIconElement = '.modal-footer #loader-icon';\r\n\r\n Preset.prototype.actionbuttons = '.modal-footer button';\r\n\r\n /**\r\n * Setup the presets modal event listeners.\r\n */\r\n Preset.prototype.setupmodal = function() {\r\n\r\n var THIS = this;\r\n\r\n var triggerelement = document.querySelectorAll('.pulse-usepreset');\r\n // Modal attachment point.\r\n var attachmentPoint = document.createElement('div');\r\n attachmentPoint.classList.add('modal-preset');\r\n triggerelement.forEach((element) => element.addEventListener('click', () => {\r\n var presetid = element.getAttribute('data-presetid');\r\n var presettitle = element.getAttribute('data-presettitle');\r\n var params = {'presetid': presetid, 'courseid': THIS.courseid, 'section': THIS.section};\r\n\r\n document.body.prepend(attachmentPoint);\r\n Modal.create({\r\n type: ModalPreset.TYPE,\r\n title: Str.get_string('presetmodaltitle', 'pulse', {'title': presettitle}),\r\n body: Fragment.loadFragment('mod_pulse', 'get_preset_preview', THIS.contextId, params),\r\n large: true,\r\n }).then(modal => {\r\n // Make the modal attachment point to overcome the restriction access condition.\r\n modal.attachmentPoint = attachmentPoint;\r\n modal.show();\r\n modal.getRoot().on(ModalEvents.bodyRendered, function() {\r\n THIS.reinitAvailability(SELECTOR.presetAvailability);\r\n THIS.fieldChangedEvent();\r\n });\r\n // Destroy the modal on hidden to reload the editors.\r\n modal.getRoot().on(ModalEvents.hidden, function() {\r\n modal.destroy.bind(modal);\r\n THIS.reinitAvailability();\r\n });\r\n\r\n // Apply and customize method.\r\n modal.getRoot().on(PresetEvents.customize, () => {\r\n var modform = document.querySelector('#mod-pulse-form');\r\n var modformdata = new FormData(modform);\r\n modal.getRoot().get(0).querySelectorAll('form').forEach(form => {\r\n var formdata = new FormData(form);\r\n formdata = new URLSearchParams(formdata).toString();\r\n var pageparams = new URLSearchParams(modformdata).toString();\r\n params = {formdata: formdata, pageparams: pageparams};\r\n\r\n Loadingicon.addIconToContainer(this.loadIconElement);\r\n THIS.disableButtons();\r\n THIS.applyCustomize(params, THIS.contextId, modal);\r\n });\r\n });\r\n // Apply and save method.\r\n modal.getRoot().on(PresetEvents.save, (e) => {\r\n e.preventDefault();\r\n Loadingicon.addIconToContainer(this.loadIconElement);\r\n THIS.disableButtons();\r\n var formdata = {};\r\n modal.getRoot().get(0).querySelectorAll('form').forEach(form => {\r\n formdata = new FormData(form);\r\n this.restorePreset(formdata, THIS.contextId);\r\n });\r\n });\r\n return true;\r\n }).catch(Notification.exception);\r\n }));\r\n };\r\n\r\n\r\n Preset.prototype.fieldChangedEvent = () => {\r\n var confParam = document.getElementById(\"preset-configurable-params\");\r\n var reminders = ['first', 'second', 'recurring'];\r\n var methods = ['fixed', 'relative'];\r\n var fieldName, changeinput, id, changeName, split;\r\n confParam.querySelectorAll('input, select, textarea').forEach(field => {\r\n field.addEventListener('change', (event) => {\r\n fieldName = event.target.getAttribute('name');\r\n if (confParam.querySelector('input[name=\"' + fieldName + '_changed\"]') !== null) {\r\n confParam.querySelector('input[name=\"' + fieldName + '_changed\"]').value = true;\r\n }\r\n });\r\n });\r\n\r\n reminders.forEach(reminder => {\r\n confParam.querySelectorAll('[name=\"' + reminder + '_schedule\"').forEach(schedule => {\r\n schedule.addEventListener('change', (e) => {\r\n changeName = e.target.getAttribute('name');\r\n changeinput = 'input[name=\"' + changeName + '_arr_changed\"]';\r\n confParam.querySelector(changeinput).value = true;\r\n });\r\n });\r\n methods.forEach(method => {\r\n id = reminder + \"_\" + method + \"date\";\r\n confParam.querySelectorAll('[name*=\"' + id + '\"]').forEach(opt => {\r\n opt.addEventListener('change', (e) => {\r\n split = e.target.getAttribute('name').split('[');\r\n changeName = (split.hasOwnProperty(1)) ? split[0] : split;\r\n changeinput = 'input[name=\"' + changeName + '_changed\"]';\r\n confParam.querySelector(changeinput).value = true;\r\n });\r\n });\r\n });\r\n });\r\n };\r\n\r\n /**\r\n * Reinitialize the availability javascript.\r\n * @param {string} selector\r\n */\r\n Preset.prototype.reinitAvailability = function(selector = '.availability-field') {\r\n if (typeof M.core_availability.form !== \"undefined\") {\r\n this.resetRestrictPlugins();\r\n document.querySelectorAll(selector).forEach((field) => field.parentNode.removeChild(field));\r\n M.core_availability.form.init();\r\n }\r\n };\r\n\r\n Preset.prototype.resetRestrictPlugins = function() {\r\n if (typeof M.core_availability.form !== \"undefined\" && document.getElementById('id_availabilityconditionsjson') !== null) {\r\n M.core_availability.form.restrictByGroup = null;\r\n var availabilityPlugins = (typeof M.core_availability.form.plugins !== 'undefined')\r\n ? M.core_availability.form.plugins : {};\r\n var plugin = '';\r\n for (var i in availabilityPlugins) {\r\n plugin = \"availability_\" + i;\r\n if (M.hasOwnProperty(plugin)) {\r\n M[plugin].form.addedEvents = false;\r\n }\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Apply and customize triggered using fragment. Response will replaced with current mod form.\r\n * @param {string} params\r\n * @param {int} contextID\r\n * @param {object} modal\r\n */\r\n Preset.prototype.applyCustomize = function(params, contextID, modal) {\r\n Fragment.loadFragment('mod_pulse', 'apply_preset', contextID, params).done((html, js) => {\r\n modal.destroy();\r\n // Reset the availability to work for upcoming response html.\r\n this.resetRestrictPlugins();\r\n this.handleFormSubmissionResponse(html, js);\r\n });\r\n };\r\n\r\n /**\r\n * Disable the modal save and customize buttons to prevent reinit.\r\n */\r\n Preset.prototype.disableButtons = function() {\r\n var buttons = document.querySelectorAll(this.actionbuttons);\r\n for (let $i in buttons) {\r\n buttons[$i].disabled = true;\r\n }\r\n };\r\n\r\n /**\r\n * Handle the loaded fragment output of customize method pulse mod.\r\n * @param {html} data\r\n * @param {string} js\r\n */\r\n Preset.prototype.handleFormSubmissionResponse = (data, js) => {\r\n var newform = document.createElement('div');\r\n newform.innerHTML = data;\r\n Templates.replaceNode('[action=\"modedit.php\"]', data, js);\r\n\r\n };\r\n\r\n /**\r\n * Initiate the apply and save method to create the pulse module with custom daa.\r\n * @param {FormData} formdata\r\n * @param {int} contextid\r\n */\r\n Preset.prototype.restorePreset = (formdata, contextid) => {\r\n var formdatastr = new URLSearchParams(formdata).toString();\r\n var promises = AJAX.call([{\r\n methodname: 'mod_pulse_apply_presets',\r\n args: {contextid: contextid, formdata: formdatastr}\r\n }]);\r\n\r\n promises[0].done((response) => {\r\n response = JSON.parse(response);\r\n if (typeof response.url != 'undefined') {\r\n window.location.href = response.url;\r\n }\r\n });\r\n };\r\n\r\n /**\r\n * Load list of available presets.\r\n */\r\n Preset.prototype.loadPresetsList = function() {\r\n var listParent = document.getElementById(this.listElement.selector);\r\n\r\n if (listParent !== null) {\r\n if (listParent.getAttribute(this.listElement.loaded) == 'false') {\r\n Fragment.loadFragment('mod_pulse', 'get_presetslist', this.contextId, {'courseid': this.courseid})\r\n .done((html, js) => {\r\n Templates.replaceNodeContents(listParent, html, js);\r\n listParent.setAttribute(this.listElement.loaded, 'true');\r\n this.setupmodal();\r\n });\r\n }\r\n }\r\n };\r\n\r\n return {\r\n init: (contextId, courseid, section) => {\r\n new Preset(contextId, courseid, section);\r\n }\r\n };\r\n});\r\n"],"names":["define","$","Modal","ModalPreset","PresetEvents","Str","Fragment","AJAX","Templates","Loadingicon","Notification","ModalEvents","SELECTOR","Preset","contextId","courseid","section","loadPresetsList","prototype","listElement","pageparams","loadIconElement","actionbuttons","setupmodal","THIS","this","triggerelement","document","querySelectorAll","attachmentPoint","createElement","classList","add","forEach","element","addEventListener","presetid","getAttribute","presettitle","params","body","prepend","create","type","TYPE","title","get_string","loadFragment","large","then","modal","show","getRoot","on","bodyRendered","reinitAvailability","fieldChangedEvent","hidden","destroy","bind","customize","modform","querySelector","modformdata","FormData","get","form","formdata","URLSearchParams","toString","addIconToContainer","disableButtons","applyCustomize","save","e","preventDefault","restorePreset","catch","exception","fieldName","changeinput","id","changeName","split","confParam","getElementById","methods","field","event","target","value","reminder","schedule","method","opt","hasOwnProperty","selector","M","core_availability","resetRestrictPlugins","parentNode","removeChild","init","restrictByGroup","availabilityPlugins","plugins","plugin","i","addedEvents","contextID","done","html","js","handleFormSubmissionResponse","buttons","$i","disabled","data","innerHTML","replaceNode","contextid","formdatastr","call","methodname","args","response","JSON","parse","url","window","location","href","listParent","loaded","replaceNodeContents","setAttribute"],"mappings":"AAAAA,0BAAO,CAAC,SAAU,qBAAsB,yBAA0B,mBAAoB,WACtF,gBAAiB,YAAa,iBAAkB,mBAAoB,oBAAqB,sBACrF,SAASC,EAAGC,MAAOC,YAAaC,aAAcC,IAAKC,SAAUC,KAAMC,UAAWC,YAAaC,aAAcC,iBAErGC,4BACoB,4CASpBC,OAAS,SAASC,UAAWC,SAAUC,cAClCF,UAAYA,eACZC,SAAWA,cACXC,QAAUA,aACVC,0BAGTJ,OAAOK,UAAUC,YAAc,UAAa,4BAAgC,mBAE5EN,OAAOK,UAAUJ,UAAY,EAE7BD,OAAOK,UAAUH,SAAW,EAE5BF,OAAOK,UAAUF,QAAU,EAE3BH,OAAOK,UAAUE,WAAa,GAE9BP,OAAOK,UAAUG,gBAAkB,6BAEnCR,OAAOK,UAAUI,cAAgB,uBAKjCT,OAAOK,UAAUK,WAAa,eAEtBC,KAAOC,KAEPC,eAAiBC,SAASC,iBAAiB,oBAE3CC,gBAAkBF,SAASG,cAAc,OAC7CD,gBAAgBE,UAAUC,IAAI,gBAC9BN,eAAeO,SAASC,SAAYA,QAAQC,iBAAiB,SAAS,SAC9DC,SAAWF,QAAQG,aAAa,iBAChCC,YAAcJ,QAAQG,aAAa,oBACnCE,OAAS,UAAaH,kBAAsBZ,KAAKT,iBAAqBS,KAAKR,SAE/EW,SAASa,KAAKC,QAAQZ,iBACtB3B,MAAMwC,OAAO,CACTC,KAAMxC,YAAYyC,KAClBC,MAAOxC,IAAIyC,WAAW,mBAAoB,QAAS,OAAUR,cAC7DE,KAAMlC,SAASyC,aAAa,YAAa,qBAAsBvB,KAAKV,UAAWyB,QAC/ES,OAAO,IACRC,MAAKC,QAEJA,MAAMrB,gBAAkBA,gBACxBqB,MAAMC,OACND,MAAME,UAAUC,GAAG1C,YAAY2C,cAAc,WACzC9B,KAAK+B,mBAAmB3C,6BACxBY,KAAKgC,uBAGTN,MAAME,UAAUC,GAAG1C,YAAY8C,QAAQ,WACnCP,MAAMQ,QAAQC,KAAKT,OACnB1B,KAAK+B,wBAITL,MAAME,UAAUC,GAAGjD,aAAawD,WAAW,SACnCC,QAAUlC,SAASmC,cAAc,mBACjCC,YAAc,IAAIC,SAASH,SAC/BX,MAAME,UAAUa,IAAI,GAAGrC,iBAAiB,QAAQK,SAAQiC,WAChDC,SAAW,IAAIH,SAASE,MAC5BC,SAAW,IAAIC,gBAAgBD,UAAUE,eACrCjD,WAAa,IAAIgD,gBAAgBL,aAAaM,WAClD9B,OAAS,CAAC4B,SAAUA,SAAU/C,WAAYA,YAE1CX,YAAY6D,mBAAmB7C,KAAKJ,iBACpCG,KAAK+C,iBACL/C,KAAKgD,eAAejC,OAAQf,KAAKV,UAAWoC,aAIpDA,MAAME,UAAUC,GAAGjD,aAAaqE,MAAOC,IACnCA,EAAEC,iBACFlE,YAAY6D,mBAAmB7C,KAAKJ,iBACpCG,KAAK+C,qBACDJ,SAAW,GACfjB,MAAME,UAAUa,IAAI,GAAGrC,iBAAiB,QAAQK,SAAQiC,OACpDC,SAAW,IAAIH,SAASE,WACnBU,cAAcT,SAAU3C,KAAKV,kBAGnC,KACR+D,MAAMnE,aAAaoE,iBAK9BjE,OAAOK,UAAUsC,kBAAoB,SAI7BuB,UAAWC,YAAaC,GAAIC,WAAYC,MAHxCC,UAAYzD,SAAS0D,eAAe,8BAEpCC,QAAU,CAAC,QAAS,YAExBF,UAAUxD,iBAAiB,2BAA2BK,SAAQsD,QAC1DA,MAAMpD,iBAAiB,UAAWqD,QAC9BT,UAAYS,MAAMC,OAAOpD,aAAa,QACqC,OAAvE+C,UAAUtB,cAAc,eAAiBiB,UAAY,gBACrDK,UAAUtB,cAAc,eAAiBiB,UAAY,cAAcW,OAAQ,SAPvE,CAAC,QAAS,SAAU,aAY1BzD,SAAQ0D,WACdP,UAAUxD,iBAAiB,UAAY+D,SAAW,cAAc1D,SAAQ2D,WACpEA,SAASzD,iBAAiB,UAAWuC,IACjCQ,WAAaR,EAAEe,OAAOpD,aAAa,QACnC2C,YAAc,eAAiBE,WAAa,iBAC5CE,UAAUtB,cAAckB,aAAaU,OAAQ,QAGrDJ,QAAQrD,SAAQ4D,SACZZ,GAAKU,SAAW,IAAME,OAAS,OAC/BT,UAAUxD,iBAAiB,WAAaqD,GAAK,MAAMhD,SAAQ6D,MACvDA,IAAI3D,iBAAiB,UAAWuC,IAC5BS,MAAQT,EAAEe,OAAOpD,aAAa,QAAQ8C,MAAM,KAC5CD,WAAcC,MAAMY,eAAe,GAAMZ,MAAM,GAAKA,MACpDH,YAAc,eAAiBE,WAAa,aAC5CE,UAAUtB,cAAckB,aAAaU,OAAQ,eAWjE7E,OAAOK,UAAUqC,mBAAqB,eAASyC,gEAAW,2BACd,IAA7BC,EAAEC,kBAAkBhC,YACtBiC,uBACLxE,SAASC,iBAAiBoE,UAAU/D,SAASsD,OAAUA,MAAMa,WAAWC,YAAYd,SACpFU,EAAEC,kBAAkBhC,KAAKoC,SAIjCzF,OAAOK,UAAUiF,qBAAuB,mBACI,IAA7BF,EAAEC,kBAAkBhC,MAAqF,OAA7DvC,SAAS0D,eAAe,iCAA2C,CACtHY,EAAEC,kBAAkBhC,KAAKqC,gBAAkB,SACvCC,yBAAmE,IAArCP,EAAEC,kBAAkBhC,KAAKuC,QACrDR,EAAEC,kBAAkBhC,KAAKuC,QAAU,GACrCC,OAAS,OACR,IAAIC,KAAKH,oBACVE,OAAS,gBAAkBC,EACvBV,EAAEF,eAAeW,UACjBT,EAAES,QAAQxC,KAAK0C,aAAc,KAY7C/F,OAAOK,UAAUsD,eAAiB,SAASjC,OAAQsE,UAAW3D,OAC1D5C,SAASyC,aAAa,YAAa,eAAgB8D,UAAWtE,QAAQuE,MAAK,CAACC,KAAMC,MAC9E9D,MAAMQ,eAEDyC,4BACAc,6BAA6BF,KAAMC,QAOhDnG,OAAOK,UAAUqD,eAAiB,eAC1B2C,QAAUvF,SAASC,iBAAiBH,KAAKH,mBACxC,IAAI6F,MAAMD,QACXA,QAAQC,IAAIC,UAAW,GAS/BvG,OAAOK,UAAU+F,6BAA+B,CAACI,KAAML,MACrCrF,SAASG,cAAc,OAC7BwF,UAAYD,KACpB7G,UAAU+G,YAAY,yBAA0BF,KAAML,KAS1DnG,OAAOK,UAAU0D,cAAgB,CAACT,SAAUqD,iBACpCC,YAAc,IAAIrD,gBAAgBD,UAAUE,WACjC9D,KAAKmH,KAAK,CAAC,CACtBC,WAAY,0BACZC,KAAM,CAACJ,UAAWA,UAAWrD,SAAUsD,gBAGlC,GAAGX,MAAMe,gBAEa,KAD3BA,SAAWC,KAAKC,MAAMF,WACFG,MAChBC,OAAOC,SAASC,KAAON,SAASG,SAQ5CnH,OAAOK,UAAUD,gBAAkB,eAC3BmH,WAAazG,SAAS0D,eAAe5D,KAAKN,YAAY6E,UAEvC,OAAfoC,YACwD,SAApDA,WAAW/F,aAAaZ,KAAKN,YAAYkH,SACzC/H,SAASyC,aAAa,YAAa,kBAAmBtB,KAAKX,UAAW,UAAaW,KAAKV,WACvF+F,MAAK,CAACC,KAAMC,MACTxG,UAAU8H,oBAAoBF,WAAYrB,KAAMC,IAChDoB,WAAWG,aAAa9G,KAAKN,YAAYkH,OAAQ,aAC5C9G,iBAMd,CACH+E,KAAM,CAACxF,UAAWC,SAAUC,eACpBH,OAAOC,UAAWC,SAAUC"} \ No newline at end of file diff --git a/amd/src/automation.js b/amd/src/automation.js new file mode 100644 index 0000000..d7a6c38 --- /dev/null +++ b/amd/src/automation.js @@ -0,0 +1,192 @@ +define("mod_pulse/automation", ['jquery', 'core/modal_factory', 'core/templates', 'core/str'], function($, Modal, Template, Str) { + + const moveOutMoreMenu = (navMenu) => { + + if (navMenu === null) { + return; + } + + var menu = navMenu.querySelector('a.automation-templates'); + + if (menu === null) { + return; + } + + menu = menu.parentNode; + menu.dataset.forceintomoremenu = false; + menu.querySelector('a').classList.remove('dropdown-item'); + menu.querySelector('a').classList.add('nav-link'); + menu.parentNode.removeChild(menu); + + // Insert the stored menus before the more menu. + navMenu.insertBefore(menu, navMenu.children[1]); + window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu. + }; + + const returnToFailedTab = () => { + + if (document.forms['pulse-automation-template'] === null) { + return false; + } + + document.forms['pulse-automation-template'].onsubmit = (e) => { + var form = e.target; + var invalidElement = form.querySelector('.is-invalid'); + if (invalidElement === null) { + return true; + } + + var tabid = invalidElement.parentNode.parentNode.parentNode.id; + var hrefSelector = '[href="#' + tabid + '"]'; + + document.querySelector(hrefSelector).click(); + + return true; + }; + + return true; + }; + + // No need. + const updateAutoCompletionPositions = function() { + var group = "checkboxgroupautomation"; + + if (document.querySelectorAll('input[type=checkbox].' + group) + === null || document.querySelectorAll('[data-fieldtype="autocomplete"]') === null) { + return true; + } + + document.querySelectorAll('[data-fieldtype="autocomplete"]').forEach((element) => { + + if (element === null) { + return true; + } + + var observer = new MutationObserver(function(mutations) { + mutations.forEach((mutation) => { + // Console.log(mutation); + // If(mutation.type === 'attributes') { + var target = mutation.target; + var overrideElement = target.querySelector('.custom-switch'); + if (overrideElement === null) { + return; + } + overrideElement.parentNode.append(overrideElement); + observer.disconnect(); + }); + }); + observer.observe(element, {attributes: true, childList: true, subtree: true}); + // Observer.disconnect(); + return true; + }); + + return true; + }; + + const moveOverRidePosition = function() { + + var group = "checkboxgroupautomation"; + + if (document.querySelectorAll('input[type=checkbox].' + group) === null) { + return true; + } + + document.querySelectorAll('input[type=checkbox].' + group).forEach((overElement) => { + var id = overElement.id; + id = id.replace('id_override_', ''); + var element = document.querySelector('div#fitem_id_' + id); + if (element === null) { + element = document.querySelector('div#fgroup_id_' + id); + if (element === null) { + return true; + } + } + + var parent = overElement.parentNode; + parent.innerHTML += ''; + + var nodeToMove = document.createElement('div'); + nodeToMove.classList.add('custom-control', 'custom-switch'); + nodeToMove.append(parent); + element.querySelector(".felement").append(nodeToMove); + return true; + }); + // Move the override button for autocompletion fields after the autocomplete nodes are created. + updateAutoCompletionPositions(); + + return true; + }; + + /** + * Create a modal to display the list of instances which is overriden the template setting. + * + * @returns {void} + */ + const overrideModal = function() { + + // Add the template reference as prefix of the instance reference. + var templateReference = document.querySelector('#pulse-template-reference'); + var instanceReference = document.querySelector('#fitem_id_insreference .felement'); + if (templateReference && instanceReference) { + templateReference.classList.remove('hide'); + instanceReference.prepend(templateReference); + } + + const trigger = document.querySelectorAll('[data-target="overridemodal"]'); + + if (trigger === null) { + return; + } + + trigger.forEach((elem) => { + + elem.nextSibling.querySelector('.felement').append(elem); + + elem.addEventListener('click', function(e) { + e.preventDefault(); + var element = e.target; + var data = element.dataset; + var instance = document.querySelector('[name=overinstance_' + data.element + ']'); + if (instance !== null) { + var overrides = JSON.parse(instance.value); + overrides.map((value) => { + var path = '/mod/pulse/automation/instances/edit.php?instanceid='; + value.url = M.cfg.wwwroot + path + value.id + '&sesskey=' + M.cfg.sesskey; + return value; + }); + Modal.create({ + title: Str.get_string('instanceoverrides', 'pulse'), + body: Template.render('mod_pulse/overrides', {instances: overrides}) + }).then((modal) => { + modal.show(); + return true; + }).catch(); + } + }); + }); + }; + + const enableTitleOnSubmit = function() { + if (document.forms['pulse-automation-template'] === null) { + return; + } + document.forms['pulse-automation-template'].onsubmit = + () => document.querySelector('[name="title"]').removeAttribute("disabled"); + }; + + return { + + init: function() { + returnToFailedTab(); + overrideModal(); + moveOverRidePosition(); + enableTitleOnSubmit(); + }, + + instanceMenuLink: function() { + var primaryNav = document.querySelector('.secondary-navigation ul.more-nav'); + moveOutMoreMenu(primaryNav); + }, + + }; +}); diff --git a/amd/src/completion.js b/amd/src/completion.js index 34df018..0cac2b5 100644 --- a/amd/src/completion.js +++ b/amd/src/completion.js @@ -17,7 +17,7 @@ * Module javascript to place the placeholders. * Modified version of IOMAD Email template emailvars. * - * @package mod_pulse + * @module mod_pulse/completion * @category Classes - autoloading * @copyright 2021, bdecent gmbh bdecent.de * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/amd/src/events.js b/amd/src/events.js new file mode 100644 index 0000000..62f84c4 --- /dev/null +++ b/amd/src/events.js @@ -0,0 +1,30 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Contain the events the data privacy tool can fire. + * + * @module mod_pulse/events + * @category Classes - autoloading + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + define([], function() { + return { + save: 'mod_pulse-preset:save', + customize: 'mod_pulse-preset:customize', + }; +}); diff --git a/amd/src/modal_preset.js b/amd/src/modal_preset.js new file mode 100644 index 0000000..c8c4854 --- /dev/null +++ b/amd/src/modal_preset.js @@ -0,0 +1,99 @@ +define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry', 'mod_pulse/events'], + function($, Notification, CustomEvents, Modal, ModalRegistry, PresetEvents) { + + var registered = false; + var SELECTORS = { + SAVE_BUTTON: '[data-action="save"]', + CUSTOMIZE_BUTTON: '[data-action="customize"]', + CANCEL_BUTTON: '[data-action="cancel"]', + }; + + /** + * Constructor for the Modal. + * + * @param {object} root The root jQuery element for the modal + */ + var ModalPreset = function(root) { + Modal.call(this, root); + + if (!this.getFooter().find(SELECTORS.SAVE_BUTTON).length) { + Notification.exception({message: 'No "Apply and save" button found'}); + } + + if (!this.getFooter().find(SELECTORS.CUSTOMIZE_BUTTON).length) { + Notification.exception({message: 'No "Apply and customize" button found'}); + } + + if (!this.getFooter().find(SELECTORS.CANCEL_BUTTON).length) { + Notification.exception({message: 'No cancel button found'}); + } + }; + + ModalPreset.TYPE = 'PresetModal'; + ModalPreset.prototype = Object.create(Modal.prototype); + ModalPreset.prototype.constructor = ModalPreset; + ModalPreset.prototype.formData = ''; + + /** + * Set up all of the event handling for the modal. + * + * @method registerEventListeners + */ + ModalPreset.prototype.registerEventListeners = function() { + // Apply parent event listeners. + Modal.prototype.registerEventListeners.call(this); + + this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(event, data) { + // Load the backupfile. + document.querySelectorAll('.preset-config-params form.mform').forEach(form => { + form.importmethod.value = 'save'; + form.addEventListener('submit', function(e) { + e.preventDefault(); + }); + }); + + if (document.querySelectorAll('.preset-config-params [data-fieldtype="submit"] input').length != 0) { + document.querySelectorAll('.preset-config-params [data-fieldtype="submit"] input')[0].click(); + } + + var approveEvent = $.Event(PresetEvents.save); + this.getRoot().trigger(approveEvent, this); + + if (!approveEvent.isDefaultPrevented()) { + this.destroy(); + data.originalEvent.preventDefault(); + } + event.preventDefault(); + }.bind(this)); + + + this.getModal().on(CustomEvents.events.activate, SELECTORS.CUSTOMIZE_BUTTON, function(event, data) { + // Add your logic for when the login button is clicked. This could include the form validation, + document.querySelectorAll('.preset-config-params form.mform').forEach(form => { + form.importmethod.value = 'customize'; + }); + + var customizeEvent = $.Event(PresetEvents.customize); + this.getRoot().trigger(customizeEvent, this); + + if (!customizeEvent.isDefaultPrevented()) { + data.originalEvent.preventDefault(); + } + event.preventDefault(); + + }.bind(this)); + + this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function() { + this.destroy(); + }.bind(this)); + }; + + // Automatically register with the modal registry the first time this module is imported so that you can create modals + // of this type using the modal factory. + if (!registered) { + ModalRegistry.register(ModalPreset.TYPE, ModalPreset, 'mod_pulse/modal_preset'); + registered = true; + } + + return ModalPreset; +}); diff --git a/amd/src/module.js b/amd/src/module.js index 37687a0..49338e5 100644 --- a/amd/src/module.js +++ b/amd/src/module.js @@ -17,43 +17,135 @@ * Module javascript to place the placeholders. * Modified version of IOMAD Email template emailvars. * - * @package mod_pulse + * @module mod_pulse/module * @category Classes - autoloading * @copyright 2021, bdecent gmbh bdecent.de * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define([], function() { +define(['core_editor/events'], function() { return { - /** * Setup the classes to editors works with placeholders. */ init: function() { var module = this; + var templatevars = document.getElementsByClassName("fitem_id_templatevars_editor"); for (var l = 0; l < templatevars.length; l++) { templatevars[l].addEventListener('click', function() { var EditorInput = document.getElementById('id_pulse_content_editoreditable'); - var caret = document.getElementsByClassName("insertatcaretactive"); - for (var j = 0; j < caret.length; j++) { - caret[j].classList.remove("insertatcaretactive"); + if (EditorInput !== null) { + module.insertCaretActive(EditorInput); } - EditorInput.classList.add("insertatcaretactive"); + }); + } + + var notificationheader = document.getElementById('admin-notificationheader'); + if (notificationheader !== null) { + notificationheader.addEventListener('click', function() { + var EditorInput = document.getElementById('id_s_mod_pulse_notificationheadereditable'); + module.insertCaretActive(EditorInput); + }); + } + + var notificationfooter = document.getElementById('admin-notificationfooter'); + if (notificationfooter !== null) { + notificationfooter.addEventListener('click', function() { + var EditorInput = document.getElementById('id_s_mod_pulse_notificationfootereditable'); + module.insertCaretActive(EditorInput); + }); + } + + templatevars = document.getElementsByClassName("fitem_id_templatevars_editor"); + if (templatevars) { + templatevars.forEach((elem) => { + elem.addEventListener('click', function(e) { + var target = e.currentTarget; + var EditorInput = target.querySelector('[id*="_editoreditable"]'); + module.insertCaretActive(EditorInput); + }); + }); + } + // Console.log(window.tinyMCE.get()); + var targetNode = document.querySelector('textarea[id$=_editor]'); + if (targetNode !== null) { + var observer = new MutationObserver(function() { + if (targetNode.style.display == 'none') { + setTimeout(initIframeListeners, 100); + } }); + observer.observe(targetNode, {attributes: true, childList: true}); } + + const initIframeListeners = () => { + + var iframes = document.querySelectorAll('[data-fieldtype="editor"] iframe'); + if (iframes === null || !iframes.length) { + return false; + } + + iframes.forEach((iframe) => { + iframe.contentDocument.addEventListener('click', function(e) { + + var currentFrame = e.target; + iframes.forEach((frame) => { + var frameElem = frame.contentDocument.querySelector(".insertatcaretactive"); + if (frameElem !== null) { + frameElem.classList.remove("insertatcaretactive"); + } + }); + + var contentBody = currentFrame.querySelector('body'); + if (contentBody !== null) { + contentBody.classList.add("insertatcaretactive"); + } + }); + }); + + return true; + }; + + var clickforword = document.getElementsByClassName('clickforword'); for (var i = 0; i < clickforword.length; i++) { clickforword[i].addEventListener('click', function(e) { e.preventDefault(); // To prevent the default behaviour of a tag. - module.insertAtCaret("{" + this.getAttribute('data-text') + "}"); + + var content = "{" + this.getAttribute('data-text') + "}"; + var iframes = document.querySelectorAll('[data-fieldtype="editor"] iframe'); + if (iframes === null || !iframes.length) { + return false; + } + var tinyEditor; + iframes.forEach((frame) => { + var frameElem = frame.contentDocument.querySelector(".insertatcaretactive"); + if (frameElem !== null) { + var contentBody = frame.contentDocument.querySelector('body'); + if (contentBody !== null) { + contentBody.classList.add("insertatcaretactive"); + var id = contentBody.dataset.id; + var editor = window.tinyMCE.get(id); + tinyEditor = editor; + } + } + return false; + }); + + if (tinyEditor) { + tinyEditor.selection.setContent(content); + } else { + module.insertAtCaret(content); + } + + return true; }); } // Make selected roles as badges in module edit form page. - if (document.getElementById('page-mod-pulse-mod') + if (document.getElementById('page-mod-pulse-mod') !== null && document.getElementById('page-mod-pulse-mod') .querySelector("#fgroup_id_completionrequireapproval [data-fieldtype='autocomplete']") !== null) { const textNodes = this.getAllTextNodes( document.getElementById('page-mod-pulse-mod') @@ -68,6 +160,17 @@ define([], function() { } }, + insertCaretActive: function(EditorInput) { + if (EditorInput === null) { + return; + } + var caret = document.getElementsByClassName("insertatcaretactive"); + for (var j = 0; j < caret.length; j++) { + caret[j].classList.remove("insertatcaretactive"); + } + EditorInput.classList.add("insertatcaretactive"); + }, + /** * Filter text from node. * @param {string} element @@ -78,6 +181,26 @@ define([], function() { .filter(node => node.nodeType === 3 && node.textContent.trim().length > 1); }, + /** + * Find the selection is inside the editor + * + * @param {string} div + * @returns {bool} + */ + isSelectionInsideDiv: (div) => { + const selection = window.getSelection(); + if (selection.rangeCount === 0) { + return false; + } + + // Get the start and end nodes of the selection. + const startNode = selection.getRangeAt(0).startContainer; + const endNode = selection.getRangeAt(0).endContainer; + + // Check if the start and end nodes are both descendants of the editor div. + return div.contains(startNode) && div.contains(endNode); + }, + /** * Insert the placeholder in selected caret place. * @param {string} myValue @@ -88,7 +211,7 @@ define([], function() { for (var n = 0; n < caretelements.length; n++) { var thiselem = caretelements[n]; - if (typeof thiselem.value === 'undefined' && window.getSelection) { + if (typeof thiselem.value === 'undefined' && window.getSelection && this.isSelectionInsideDiv(thiselem)) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); diff --git a/amd/src/preset.js b/amd/src/preset.js new file mode 100644 index 0000000..d74dbe4 --- /dev/null +++ b/amd/src/preset.js @@ -0,0 +1,247 @@ +define(['jquery', 'core/modal_factory', 'mod_pulse/modal_preset', 'mod_pulse/events', 'core/str', +'core/fragment', 'core/ajax', 'core/templates', 'core/loadingicon', 'core/notification', 'core/modal_events'], + function($, Modal, ModalPreset, PresetEvents, Str, Fragment, AJAX, Templates, Loadingicon, Notification, ModalEvents) { + + var SELECTOR = { + presetAvailability: '.preset-config-params .availability-field' + }; + + /** + * Preset module declaration. Setup the global values. + * @param {int} contextId + * @param {int} courseid + * @param {int} section + */ + var Preset = function(contextId, courseid, section) { + this.contextId = contextId; + this.courseid = courseid; + this.section = section; + this.loadPresetsList(); + }; + + Preset.prototype.listElement = {'selector': 'pulse-presets-data', "loaded": "data-listloaded"}; + + Preset.prototype.contextId = 0; + + Preset.prototype.courseid = 0; + + Preset.prototype.section = 0; + + Preset.prototype.pageparams = []; + + Preset.prototype.loadIconElement = '.modal-footer #loader-icon'; + + Preset.prototype.actionbuttons = '.modal-footer button'; + + /** + * Setup the presets modal event listeners. + */ + Preset.prototype.setupmodal = function() { + + var THIS = this; + + var triggerelement = document.querySelectorAll('.pulse-usepreset'); + // Modal attachment point. + var attachmentPoint = document.createElement('div'); + attachmentPoint.classList.add('modal-preset'); + triggerelement.forEach((element) => element.addEventListener('click', () => { + var presetid = element.getAttribute('data-presetid'); + var presettitle = element.getAttribute('data-presettitle'); + var params = {'presetid': presetid, 'courseid': THIS.courseid, 'section': THIS.section}; + + document.body.prepend(attachmentPoint); + Modal.create({ + type: ModalPreset.TYPE, + title: Str.get_string('presetmodaltitle', 'pulse', {'title': presettitle}), + body: Fragment.loadFragment('mod_pulse', 'get_preset_preview', THIS.contextId, params), + large: true, + }).then(modal => { + // Make the modal attachment point to overcome the restriction access condition. + modal.attachmentPoint = attachmentPoint; + modal.show(); + modal.getRoot().on(ModalEvents.bodyRendered, function() { + THIS.reinitAvailability(SELECTOR.presetAvailability); + THIS.fieldChangedEvent(); + }); + // Destroy the modal on hidden to reload the editors. + modal.getRoot().on(ModalEvents.hidden, function() { + modal.destroy.bind(modal); + THIS.reinitAvailability(); + }); + + // Apply and customize method. + modal.getRoot().on(PresetEvents.customize, () => { + var modform = document.querySelector('#mod-pulse-form'); + var modformdata = new FormData(modform); + modal.getRoot().get(0).querySelectorAll('form').forEach(form => { + var formdata = new FormData(form); + formdata = new URLSearchParams(formdata).toString(); + var pageparams = new URLSearchParams(modformdata).toString(); + params = {formdata: formdata, pageparams: pageparams}; + + Loadingicon.addIconToContainer(this.loadIconElement); + THIS.disableButtons(); + THIS.applyCustomize(params, THIS.contextId, modal); + }); + }); + // Apply and save method. + modal.getRoot().on(PresetEvents.save, (e) => { + e.preventDefault(); + Loadingicon.addIconToContainer(this.loadIconElement); + THIS.disableButtons(); + var formdata = {}; + modal.getRoot().get(0).querySelectorAll('form').forEach(form => { + formdata = new FormData(form); + this.restorePreset(formdata, THIS.contextId); + }); + }); + return true; + }).catch(Notification.exception); + })); + }; + + + Preset.prototype.fieldChangedEvent = () => { + var confParam = document.getElementById("preset-configurable-params"); + var reminders = ['first', 'second', 'recurring']; + var methods = ['fixed', 'relative']; + var fieldName, changeinput, id, changeName, split; + confParam.querySelectorAll('input, select, textarea').forEach(field => { + field.addEventListener('change', (event) => { + fieldName = event.target.getAttribute('name'); + if (confParam.querySelector('input[name="' + fieldName + '_changed"]') !== null) { + confParam.querySelector('input[name="' + fieldName + '_changed"]').value = true; + } + }); + }); + + reminders.forEach(reminder => { + confParam.querySelectorAll('[name="' + reminder + '_schedule"').forEach(schedule => { + schedule.addEventListener('change', (e) => { + changeName = e.target.getAttribute('name'); + changeinput = 'input[name="' + changeName + '_arr_changed"]'; + confParam.querySelector(changeinput).value = true; + }); + }); + methods.forEach(method => { + id = reminder + "_" + method + "date"; + confParam.querySelectorAll('[name*="' + id + '"]').forEach(opt => { + opt.addEventListener('change', (e) => { + split = e.target.getAttribute('name').split('['); + changeName = (split.hasOwnProperty(1)) ? split[0] : split; + changeinput = 'input[name="' + changeName + '_changed"]'; + confParam.querySelector(changeinput).value = true; + }); + }); + }); + }); + }; + + /** + * Reinitialize the availability javascript. + * @param {string} selector + */ + Preset.prototype.reinitAvailability = function(selector = '.availability-field') { + if (typeof M.core_availability.form !== "undefined") { + this.resetRestrictPlugins(); + document.querySelectorAll(selector).forEach((field) => field.parentNode.removeChild(field)); + M.core_availability.form.init(); + } + }; + + Preset.prototype.resetRestrictPlugins = function() { + if (typeof M.core_availability.form !== "undefined" && document.getElementById('id_availabilityconditionsjson') !== null) { + M.core_availability.form.restrictByGroup = null; + var availabilityPlugins = (typeof M.core_availability.form.plugins !== 'undefined') + ? M.core_availability.form.plugins : {}; + var plugin = ''; + for (var i in availabilityPlugins) { + plugin = "availability_" + i; + if (M.hasOwnProperty(plugin)) { + M[plugin].form.addedEvents = false; + } + } + } + }; + + /** + * Apply and customize triggered using fragment. Response will replaced with current mod form. + * @param {string} params + * @param {int} contextID + * @param {object} modal + */ + Preset.prototype.applyCustomize = function(params, contextID, modal) { + Fragment.loadFragment('mod_pulse', 'apply_preset', contextID, params).done((html, js) => { + modal.destroy(); + // Reset the availability to work for upcoming response html. + this.resetRestrictPlugins(); + this.handleFormSubmissionResponse(html, js); + }); + }; + + /** + * Disable the modal save and customize buttons to prevent reinit. + */ + Preset.prototype.disableButtons = function() { + var buttons = document.querySelectorAll(this.actionbuttons); + for (let $i in buttons) { + buttons[$i].disabled = true; + } + }; + + /** + * Handle the loaded fragment output of customize method pulse mod. + * @param {html} data + * @param {string} js + */ + Preset.prototype.handleFormSubmissionResponse = (data, js) => { + var newform = document.createElement('div'); + newform.innerHTML = data; + Templates.replaceNode('[action="modedit.php"]', data, js); + + }; + + /** + * Initiate the apply and save method to create the pulse module with custom daa. + * @param {FormData} formdata + * @param {int} contextid + */ + Preset.prototype.restorePreset = (formdata, contextid) => { + var formdatastr = new URLSearchParams(formdata).toString(); + var promises = AJAX.call([{ + methodname: 'mod_pulse_apply_presets', + args: {contextid: contextid, formdata: formdatastr} + }]); + + promises[0].done((response) => { + response = JSON.parse(response); + if (typeof response.url != 'undefined') { + window.location.href = response.url; + } + }); + }; + + /** + * Load list of available presets. + */ + Preset.prototype.loadPresetsList = function() { + var listParent = document.getElementById(this.listElement.selector); + + if (listParent !== null) { + if (listParent.getAttribute(this.listElement.loaded) == 'false') { + Fragment.loadFragment('mod_pulse', 'get_presetslist', this.contextId, {'courseid': this.courseid}) + .done((html, js) => { + Templates.replaceNodeContents(listParent, html, js); + listParent.setAttribute(this.listElement.loaded, 'true'); + this.setupmodal(); + }); + } + } + }; + + return { + init: (contextId, courseid, section) => { + new Preset(contextId, courseid, section); + } + }; +}); diff --git a/approve.php b/approve.php index 854f77b..54fcae6 100644 --- a/approve.php +++ b/approve.php @@ -50,7 +50,7 @@ } } $approvalroles = $pulse->completionapprovalroles; -$hasrole = pulse_has_approvalrole($approvalroles, $cmid); +$hasrole = \mod_pulse\helper::pulse_has_approvalrole($approvalroles, $cmid); require_login(); diff --git a/assets/preset-congratulations.mbz b/assets/preset-congratulations.mbz new file mode 100644 index 0000000000000000000000000000000000000000..654e0b156036b5a5088a75fd5507ecc7083fa48c GIT binary patch literal 5321 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 Ce{T)| literal 0 HcmV?d00001 diff --git a/assets/preset-teacher_approval.mbz b/assets/preset-teacher_approval.mbz new file mode 100644 index 0000000000000000000000000000000000000000..0026b3138d29686d8d65eb80e71db0397324ad98 GIT binary patch literal 5163 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 literal 0 HcmV?d00001 diff --git a/assets/preset-welcome_message.mbz b/assets/preset-welcome_message.mbz new file mode 100644 index 0000000000000000000000000000000000000000..516c7aeb708ceb2128a8d4a57664a6fb576f1488 GIT binary patch literal 5291 zcmV;c6jbXUiwFP!000001MEF%liN13{%ZdUWNItBTQ4)>O=&DEn|SSt>p6Z~>*O8f za;ZQPk`Pm*mdDH}sr~N{2SEZPB}!w7aP3Z1a(BXs%`%rbXd8f!dFNcAn~}E>AOYjM z(be5Le7tzOa5k2S450rM63re>L)36eNOX^}uT-A?>v0)r}ntsjy(I;T2szd$%0vTQ zN3&Y+_7JTsP1G)!@$HB#CvlGUz1l+V-ybpZRO^ zziw%dfPJrmECQbvdVubZ0ji0Cn8J`ykx{<(P%_iX=mJ1da8LxM zqtZ*^FbGgEEA$oDFsCC9VcGgb^I_RB56gz=vr$H2g^vPnaEiSU&GhL=cqMQgaV>WP zmokb1meIpN#tL~=n4(HZ!m3jR$|yp2LJJYcLo~9|!)c@xQX`p~j5D+@DD$Vkz4`U^ z>%aX;PEyBFbGX6iRxN@-R3vn_AsvGP6|sb}{oFQ&Igr{e|Eu!&js+~C_{G{MX`)ApB-3~$b&+dRH4#yX4%u?(?$VjGsbfU0sn(S zx;_U#qckOAb~+N%36tbm63@O3v!^NXM9XiN*;9d^#UNWD!1Miv;*2UJD*9rQBNahT#7IHr7+Qf12ypW3BFvHtkY?ME5R8)P z5{7sgU4sxUvZoicdOzWB;d=8l^3u)IKYmS7GJlDaGyI5t}=6W>xgO`>k;lD2c;rb=Yo?Xnn zkhGm8k>XjQP~_t!!gr5~`~)|fEeXRUA`Sbd%I}Z2h-e;iuSzs8;$A}$RTeJBnHWaYnW`OBLOI*F{#Up zDq+__?57NYfeKNOrhW+1l<0P)R|pBAWE2WSh*aI90@o9yC|szx4ARJVvq|C`7}l~C z&|T>4^WW^<`?f?pj;Ew!n@|cbu%xA0L`zj73dt8|ESM9f7wJcBK-M{`E=W<^!F()i zguo+^F!o`{dzgT^Sk^B@v`9h}(1}%KiUTRuPf&m})>(zG5jQ8ZYnG>srYfXDB_fIG z_-vpML?zrrD@L7+DL@r6CQ2(Qhkp7@kdHT*ehQ!2RIIl}R0$W98A;Tdt_s)+qz0*@ z3N{po3R^buD%NZR*At1kL|`j(i&EdRDOH7cwl4Koh^fwH>q+ijbs(!Yk-z|OQdhY znmJ3vuY(%M?c{Gnwp2>(fHttqS$>`*WO7nEp#|EOr0R+$Y!80bA-kg55?9^QgtfOv z>YP?^dn%QBJ7R{gOrM#uV(en4uh|15V{|MlSxUH_G;4D-!nk|M!D0UEWy7>MX^L>pcUPAd=2$T|3 z%~Vp}4^5V@#vNwq^l@c$@xN}@_y25T`2O3EJCy&GAt7-#pDJX=7Lchrj0TqeBwd%q z;wj4L;(yJp@BdhuqYwPwhdY%2iy=YTT6iD2>VrFhQw@;MQ%2|fuh;c|&DETN|NC%< z@;~1io6DODV!KubK6G;iccd%q_Rm@do8phL|I}Q=wFdjoK3wBW$r;M%kpG6G)%8E! zG>7j$y}0A#KieX2jf6O9rRe+@<<0dQi z=1g-ko;Vs!UqUGkip1`nW7~$!Fa(Sl%fjRX-)$>=9C+t#GhUt2Pa{IKg#MPDqM~tf zP&6b#8$#={B_g=TwzbP1m4*nld^Mg!C{l4MOtoEY0$KfGv?M*pewaz;3X$%fj`;7B z!brlWCQ95!!kk!+YwFxaLcj%{Y)qjb%ElDa1hLaw+)}j)2zrT6kF-Zhs;NMMT4_0S zFBLO85@D>`8pj6!HA#^aB%Ka`8ca2Ks{3j95e{+xk)6UHzA zDIpQ2d$%A?01hehgrL}Q{*I(ZuxTJD8(r1ykw>Ebt=7quz+`5L*JL1uK@p`wN^8dl zmH(~hMNeGD{`sHCM#rkn|At`=-+y{>htB`i04d}e2?{GT7T`xi9IZ4T28CQZ6%PE zu!@weh#s?|hdjoPrIol9xNIOxO(Yc4D9V6{-oP}yjgvs4K?xE#ew=2?!HzVm;_@UR z{U@KeC_w^;wvLwCqT{R@i2T)j)g*uXTk!Tu7YAl>o`@C{&R@1I>n@g+Uh zwFu+eax1UA0wLYNNbLMA;wZaJ@$cvwXjc={wG*^{3K(<&*Erl>gG;#Cgy=HerWsm) z2Y#ftJNyj&w=DcJrrU7r0)&6%arX2rTE+ z`$K|ZD5Jh1Nx@qfr7~9Ke~mqq@Q$t{r6hbwq#<6&34WlB0ng)*42u`*_&401uV}|X zIB&OWFAm$0Sp8dzt1j1A!5aiTgd)JCFAC^xsdU>0wO+Ue@8}PpQuh57pvi7=7=j3K z^e|qM)YRw4(E=xHdQC`D<`S0~U4b7FfWZw;X~GrEfVU;X2KyiA{T*>sB8s%MzX81% z0S6=nF1CTu8sd;$MFJCw`XP#MLli9eB_J^4_kYl#w)Zv;>UnZ`De_@E!YD~&){@&* zyp~J5g(<0dK=_u{9Yz6T?h31#y8sJi^fHb>Dx8!=OJFVX5Md5ZLpBE>+V;P^V}70o zonPMn^-4-Ed4G8R{Keb1;PtCF|0!K{@`RzRlFHA(?|8q0JPF^okzz-(R|`g9Qd5j> z5bvz57%vI(5k1n(o4)23?9lE(NkXVX{wP67n2nHl0qF5fGPy=R68q+CA07G`9b5T^ znw)S#zn5-`VYL|%(ZaXWKY1O?a;yovMeYzCOF=%JjwLi1YAhh3opS%Teh>61^A@i2 z{Ac6*x31gPu>aqO8_s`!-EQwK?aQ8m{F&6`+^&m(U`_K&> z++Npwx8d|6o7vxTS)k~}Eoc|m5zYza${;wA|-2d`* zJLOrmGnLWB|F&5_|EpQ<@cpkBcNG7Z*S^6lOR$%VgGFFi9(+Eo@i^)k%Gl5Ux?x#bL;oMv z|NXeb-+zklt2PXKVZ_FC%@|Hi3GcW83uGZe~%4GsYenr7E`ih)p>#k;A zX*zw{ONmvIB#7A@%*-oYzcT*={N=|Shru;qSxj{IuVr$9$&>S{H%ru}qgs@LFBey0 zw&pDE0x90hEs-np!s$9f>?w&jL;M*Jdc{W_#!+$PhdI$L%IiKNiO(g&+VeAUuXy{RQcqR3RfT}K5S-+W$gZGQvW zUU1*hfLcXLKvjGBSv?6*$jLGw@c^q?O%&CtD^*r-OQU+V7yU-+;G3!o_6i^s5~3Rv z&gg_fFTA4uDvpg8I+sz8x`Hp)@cYyeX>rfVrWAgk%HVbC`&3V8t&4V!BAL$6XMr$^ zqwO01zKh~#DYR}k)@y@Haj>IQ{T!uwVeEhCwVh0{?76_K=fO*aK=0$ngTb<2!upUN zYk~=h>$$B=8dYXseHI%r(lzfGoJ_j=~n4I(Oo-!p~z z0ReVZU%$;|`VsBd!rLncRTqk$yx2k~ZI&#wS4Gcl=+?Y~63)E%ma(gQ?ms7=yrxtp z(vM{)r>R)EG^<*!X{5zZb4fR?mS7a+d?Y<}ML&+BWw6-5P(Eaq9}kN!oe%we_S`Pc zGrBN3>keS|{$FGN-yH0J`*DYU{}oAG%3{JHnmy;BVo&_}{XehW)=Yy#LgTJL>$eGWi33^oJSN*~+VVP!mz*e;NJxuB)YY)Ctpx$~`f;pyq34h~Cep(pGI8NwO^>k@g26!Y){1svw zdSy*25L*fG(@MpO$dX`q|E&ktW@m8fv2b+%>$+n*L;v^TTKm87_x1j><<#x}Ep2%Jy&re* z{LhC3)Aq>_;V=Fb5A)HBEvqN`cHCWj|EK#>`f6L`BW`w9hG^7|H|^jJ#3A%>h_mf) zDN#$R>NKJ#{(;ZQRl7Y8-D{_Ft^EH9y#H;w(@@7gr3Z5up)@%ESZ;2q`m_xx4OD8rDQrRAfA-TIlt zVTcmYWnllLvz!2V`SLvw$91K=`7Pn}9#>t{4$g=^`dQ0p6T;{;6)}&f2)*@qOLL;d z1930fY3gAeQ4f(3weJDU)PstAnoD;Ud=U66#4q<{Eg=d+o)9f+XKLq|me_k*;+&=$ z51Cq!0N&InK$;$S2dDR8=;tA + + + Welcome Message + A personalized welcome message sent to students upon enrolment. + In this preset, you can customize what's shown on the course page (General > Content), the subject of the notification (Invitation > Notification subject) and the body of the notification (Invitation > Notification content). If you add nothing, the default values will be used. + ["name","introeditor","pulse_subject","pulse_content_editor"] + preset-welcome_message.mbz + 1 + core:i/email + 1 + + + Teacher Approval + Require approval from the teacher of the course. + E.g. to prevent students from accessing contents of the course or for workflows that require the teacher to manually confirm something. You can customize the content shown on the course page to inform the students about the approval step. If you leave it empty, the default text will be used. + ["introeditor"] + preset-teacher_approval.mbz + 1 + core:i/lock + 2 + + + Disclaimer/Consent + Use this preset to get consent from your students. + Add the dislaimer text in the content. + ["introeditor"] + preset-disclaimer.mbz + 1 + core:req + 3 + + diff --git a/automation/automationlib.php b/automation/automationlib.php new file mode 100644 index 0000000..2591e0d --- /dev/null +++ b/automation/automationlib.php @@ -0,0 +1,94 @@ +. + +/** + * Notification pulse action - Automation lib. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('No direct access'); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Template add instance form. + */ +class template_addinstance_form extends \moodleform { + + /** + * Definition of the form elements. + * + * @return void + */ + public function definition() { + $mform =& $this->_form; + + $mform->updateAttributes(['class' => 'form-inline']); + + // Current course id. + $courseid = $this->_customdata['courseid'] ?? 0; + $mform->addElement('hidden', 'courseid', $courseid); + $mform->setType('courseid', PARAM_INT); + + // List of templates to create instance. + $templates = mod_pulse\automation\helper::get_templates_forinstance($courseid); + if (!empty($templates)) { + $mform->addElement('select', 'templateid', '', $templates); + } + + $this->add_action_buttons(false, get_string('addtemplatebtn', 'pulse')); + } +} + +/** + * Filter form for the templates table. + */ +class template_table_filter extends \moodleform { + + /** + * Filter form elements defined. + * + * @return void + */ + public function definition() { + $mform =& $this->_form; + + $mform->addElement('html', html_writer::tag('h3', get_string('filter'))); + $list = [0 => get_string('all')] + core_course_category::make_categories_list(); + $mform->addElement('autocomplete', 'category', get_string('category'), $list); + + $this->add_action_buttons(false, get_string('filter')); + } +} + +/** + * Course context class to create a context_course instance from record. + */ +class mod_pulse_context_course extends \context_course { + + /** + * Convert the record of context into course_context object. + * + * @param stdclass $data + * @return void + */ + public static function create_instance_fromrecord($data) { + return \context::create_instance_from_record($data); + } +} diff --git a/automation/instances/edit.php b/automation/instances/edit.php new file mode 100644 index 0000000..d55664e --- /dev/null +++ b/automation/instances/edit.php @@ -0,0 +1,154 @@ +. + +/** + * Mod pulse - Edit template. + * + * @package mod_pulse + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_pulse\automation\condition_base; + +// Require config. +require(__DIR__.'/../../../../config.php'); + +require_login(); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +require_sesskey(); + +// Automation template ID to edit. +$templateid = optional_param('templateid', null, PARAM_INT); +$courseid = optional_param('courseid', null, PARAM_INT); +$instanceid = optional_param('instanceid', null, PARAM_INT); + +if ($courseid) { + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); +} else { + $instanceid = required_param('instanceid', PARAM_INT); +} + +// Create the page url. +$url = new moodle_url('/mod/pulse/automation/instances/edit.php', ['sesskey' => sesskey()]); + +// Include the instance id to the page url params. +if ($instanceid) { + if ($instance = $DB->get_record('pulse_autoinstances', ['id' => $instanceid], '*', MUST_EXIST)) { + $course = $DB->get_record('course', ['id' => $instance->courseid], '*', MUST_EXIST); + $templateid = $instance->templateid; + } + $url->param('instanceid', $instanceid); + +} +// Include the course id to the page url params if exists. +if (isset($course->id)) { + $url->param('courseid', $course->id); +} + +if ($templateid) { + $url->params(['templateid' => $templateid]); + $templatereference = mod_pulse\automation\templates::create($templateid)->get_formdata()->reference; +} + +// Page values. +$context = \context_course::instance($course->id); +// Verify the user capability. +require_capability('mod/pulse:addtemplateinstance', $context); + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_course(get_course($course->id)); + +// Edit automation templates form. +$templatesform = new \mod_pulse\forms\automation_instance_form(null, [ + 'templateid' => $templateid, + 'courseid' => $course->id, + 'instanceid' => $instanceid, + 'templatereference' => $templatereference ?? '' +]); + +// Instance list page url for this course. +$overviewurl = new moodle_url('/mod/pulse/automation/instances/list.php', ['courseid' => $course->id, 'sesskey' => sesskey()]); + +// Instance form submitted, handel the submitted data. +if ($formdata = $templatesform->get_data()) { + $result = mod_pulse\automation\instances::manage_instance($formdata); + // Redirect to instances list. + redirect($overviewurl); +} else if ($templatesform->is_cancelled()) { + // Form cancelled redirect to list page. + redirect($overviewurl); +} + +// Setup the tempalte data to the form, if the form id param available. +if ($instanceid !== null && $instanceid > 0) { + + if ($record = mod_pulse\automation\instances::create($instanceid)->get_instance_formdata()) { + // Set the template data to the templates edit form. + $templatesform->set_data($record); + } else { + // Direct the user to list page with error message, when the requested menu is not available. + \core\notification::error(get_string('templatesrecordmissing', 'pulse')); + redirect($overviewurl); + } + +} else { + // Instance not created and initiated the new instance from the template, + // Then fetch the template data with actions data and assign to the template form. + $courseid = required_param('courseid', PARAM_INT); + $templateid = required_param('templateid', PARAM_INT); + + if ($record = mod_pulse\automation\templates::create($templateid)->get_template()) { + // Attach the course id to the templates. + $record->courseid = $courseid; + // Convert the trigger conditions to separate element. + $conditions = $record->triggerconditions; + foreach ($conditions as $condition) { + $record->{'condition['.$condition.'][status]'} = condition_base::ALL; + } + // Set the template data to the templates edit form. + $templatesform->set_data($record); + } else { + // Direct the user to list page with error message, when the requested template instance is not available. + \core\notification::error(get_string('templatesrecordmissing', 'pulse')); + redirect($overviewurl); + } + +} +// Template edit page heading. +$PAGE->set_heading(format_string($course->fullname)); +// PAGE breadcrumbs. +$PAGE->navbar->add(get_string('mycourses', 'core'), new moodle_url('/course/index.php')); +$PAGE->navbar->add(format_string($course->shortname), new moodle_url('/course/view.php', array('id' => $course->id))); +$PAGE->navbar->add(get_string('autotemplates', 'pulse'), $overviewurl); +$PAGE->navbar->add(get_string('autoinstances', 'pulse')); + +// Page content display started. +echo $OUTPUT->header(); + +// Template heading. +echo $OUTPUT->heading(get_string('editinstance', 'pulse')); + +// Display the template form for create or edit. +echo $templatesform->display(); + +// Footer. +echo $OUTPUT->footer(); diff --git a/automation/instances/list.php b/automation/instances/list.php new file mode 100644 index 0000000..f880e17 --- /dev/null +++ b/automation/instances/list.php @@ -0,0 +1,197 @@ +. + +/** + * Mod pulse - List the available template and manage the template Create, Update, Delete actions, sort the order of templates. + * + * @package mod_pulse + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_pulse\automation\helper; + +// Require config. +require(__DIR__.'/../../../../config.php'); + +require_login(); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$action = optional_param('action', null, PARAM_ALPHAEXT); + +$courseid = optional_param('courseid', null, PARAM_INT); +$instanceid = optional_param('instanceid', null, PARAM_INT); + +// Find the courseid. +if ($courseid) { + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); +} else { + $instanceid = required_param('instanceid', PARAM_INT); + if ($instance = $DB->get_record('pulse_autoinstances', ['id' => $instanceid], '*', MUST_EXIST)) { + $course = $DB->get_record('course', ['id' => $instance->courseid], '*', MUST_EXIST); + $courseid = $course->id; + } +} + +if (!($course = $DB->get_record('course', ['id' => $courseid]))) { + throw new moodle_exception('coursenotfound', 'core_course'); +} + +// Page values. +$context = \context_course::instance($courseid); +// Verify the user capability. +require_capability('mod/pulse:addtemplateinstance', $context); + +// Prepare the page. +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/mod/pulse/automation/instances/list.php', ['courseid' => $courseid])); +$PAGE->set_course($course); + +// Process actions. +if ($action !== null && confirm_sesskey()) { + // Every action is based on a template, thus the template ID param has to exist. + $instanceid = required_param('instanceid', PARAM_INT); + + // Create template instance. Actions are performed in template instance. + $instance = mod_pulse\automation\instances::create($instanceid); + + $transaction = $DB->start_delegated_transaction(); + + // Perform the requested action. + switch ($action) { + // Triggered action is delete, then init the deletion of template. + case 'delete': + // Delete the template. + if ($instance->delete_instance()) { + // Notification to user for template deleted success. + \core\notification::success(get_string('templatedeleted', 'pulse')); + } + break; + case 'disable': + // Disable the template visibility. + $instance->update_status(false); + break; + + case 'enable': + // Disable the template visibility. + $instance->update_status(true); + break; + + case 'copy': + // Duplicate the instance. + $instance->duplicate(); + break; + + case 'report': + $redirecturl = $instance->get_report_url(); + break; + } + + // Allow to update the changes to database. + $transaction->allow_commit(); + + // Redirect to the same page to view the templates list. + redirect($redirecturl ?? $PAGE->url); +} + +$PAGE->add_body_class('mod-pulse-automation-table'); + +// Further prepare the page. +$PAGE->set_heading(format_string($course->fullname)); + +$PAGE->navbar->add(get_string('mycourses', 'core'), new moodle_url('/course/index.php')); +$PAGE->navbar->add(format_string($course->shortname), new moodle_url('/admin/category.php', array('category' => 'mod_pulse'))); +$PAGE->navbar->add(get_string('autotemplates', 'pulse'), new moodle_url('/mod/pulse/automation/instances/list.php')); + +// Build automation templates table. +$filterset = new mod_pulse\table\automation_filterset; + +$table = new mod_pulse\table\auto_instances($context->id); +$table->define_baseurl($PAGE->url); +$table->set_filterset($filterset); + +// Start page output. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('automation', 'pulse')); + +// Show smart menus description. +echo get_string('autotemplates_desc', 'pulse'); + +// Prepare 'Create smart menu' button. // TODO Review. +$createbutton = $OUTPUT->box_start(); +$createbutton .= mod_pulse\automation\helper::get_addtemplate_instance($courseid); +$createbutton .= $OUTPUT->box_end(); + +// If there aren't any smart menus yet. +$countmenus = $DB->count_records('pulse_autotemplates'); +if ($countmenus < 1) { + // Show the table, which, since it is empty, falls back to the + // "There aren't any smart menus created yet. Please create your first smart menu to get things going." notice. + $table->out(0, true); + + // And then show the button. + echo $createbutton; + + // Otherwise. +} else { + // Show the button. + echo $createbutton; + + // And then show the table. + $table->out(10, true); + + echo helper::get_instance_tablehelps(); + + $PAGE->requires->js_amd_inline('require(["jquery"], function($) { + var notes = document.querySelectorAll("[data-target=notes-collapse]"); + + if (notes !== null) { + + notes.forEach((note) => { + note.addEventListener("click", function(e) { + e.preventDefault(); + var target = e.target.closest("[data-target=notes-collapse]"); + var collapse = target.dataset.collapse; + var tbody = target.parentNode.parentNode.parentNode; + if (target.dataset.notes == "") { + return true; + } + + if (collapse == "1") { + // target.classList.add("show"); + var trNode = document.createElement("tr"); + trNode.id = "notes_"+target.dataset.instance; + trNode.innerHTML = ""+target.dataset.notes+""; + tbody.insertBefore(trNode, target.parentNode.parentNode.nextSibling); + target.dataset.collapse = 0; + } else { + var id = "#notes_"+target.dataset.instance; + document.querySelector(id).remove(); + target.dataset.collapse = 1; + } + target.childNodes[0].classList.toggle("fa-angle-right"); + target.childNodes[0].classList.toggle("fa-angle-down"); + }) + }); + } + })'); +} + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/automation/templates/edit.php b/automation/templates/edit.php new file mode 100644 index 0000000..0576b53 --- /dev/null +++ b/automation/templates/edit.php @@ -0,0 +1,106 @@ +. + +/** + * Mod pulse - Edit template. + * + * @package mod_pulse + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_pulse\automation\helper; + +// Require config. +require(__DIR__.'/../../../../config.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// User access checks. +require_sesskey(); + +// Verify the user capability. +$context = \context_system::instance(); +require_capability('mod/pulse:viewtemplateslist', $context); + +// Automation template ID to edit. +$id = optional_param('id', null, PARAM_INT); + +// Extend the features of admin settings. +admin_externalpage_setup('pulseautomation'); + +// Page values. +$url = new moodle_url('/mod/pulse/automation/templates/edit.php', ['id' => $id, 'sesskey' => sesskey()]); + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_heading(get_string('autotemplates', 'pulse')); + +// PAGE breadcrumbs. +$PAGE->navbar->add(get_string('mycourses', 'core'), new moodle_url('/course/index.php')); +$PAGE->navbar->add(get_string('autotemplates', 'pulse'), new moodle_url('/mod/pulse/automation/templates/list.php')); +$PAGE->navbar->add(get_string('edit')); + +// Edit automation templates form. +$templatesform = new \mod_pulse\forms\automation_template_form(null, ['id' => $id]); + +// Template list url. +$overviewurl = new moodle_url('/mod/pulse/automation/templates/list.php'); + +// Handling the templates form submitted data. +if ($formdata = $templatesform->get_data()) { + + // Create and update the template. + $result = mod_pulse\automation\templates::manage_instance($formdata); + + // Redirect to templates list. + redirect($overviewurl); + +} else if ($templatesform->is_cancelled()) { + // Form cancelled, redirect to the templates list. + redirect($overviewurl); +} + +// Setup the tempalte data to the form, if the form id param available. +if ($id !== null && $id > 0) { + + // Fetch the data of the template and its conditions and actions. + if ($record = mod_pulse\automation\templates::create($id)->get_template()) { + // Set the template data to the templates edit form. + $templatesform->set_data($record); + } else { + // Direct the user to list page with error message, when the requested template is not available. + \core\notification::error(get_string('templatesrecordmissing', 'pulse')); + redirect($overviewurl); + } + +} else { + // Trigger the prepare file areas for the new template create. + $templatesform->set_data([]); +} +// Page content display started. +echo $OUTPUT->header(); + +// Templates heading. +echo $OUTPUT->heading(get_string('templatessettings', 'pulse')); + +// Display the templates form for create or edit. +echo $templatesform->display(); + +// Footer. +echo $OUTPUT->footer(); diff --git a/automation/templates/list.php b/automation/templates/list.php new file mode 100644 index 0000000..0161413 --- /dev/null +++ b/automation/templates/list.php @@ -0,0 +1,205 @@ +. + +/** + * Mod pulse - List the available template and manage the template Create, Update, Delete actions, sort the order of templates. + * + * @package mod_pulse + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_pulse\automation\helper; + +// Require config. +require(__DIR__.'/../../../../config.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$action = optional_param('action', null, PARAM_ALPHAEXT); +$templateid = optional_param('id', null, PARAM_INT); +$instance = optional_param('instance', false, PARAM_BOOL); + +// Page values. +$context = \context_system::instance(); + +admin_externalpage_setup('pulseautomation'); + +// Verify the user capability. +require_capability('mod/pulse:addtemplate', $context); + +// Prepare the page. +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/mod/pulse/automation/templates/list.php')); + +// TODO: Capability checks. + +// Process actions. +if ($action !== null && confirm_sesskey()) { + // Every action is based on a template, thus the template ID param has to exist. + $templateid = required_param('id', PARAM_INT); + + // Create template instance. Actions are performed in template instance. + $template = mod_pulse\automation\templates::create($templateid); + + $transaction = $DB->start_delegated_transaction(); + + // Perform the requested action. + switch ($action) { + // Triggered action is delete, then init the deletion of template. + case 'delete': + // Delete the template. + if ($template->delete_template()) { + // Notification to user for template deleted success. + \core\notification::success(get_string('templatedeleted', 'pulse')); + } + break; + case "hidemenu": + // Disable the template visibility. + $template->update_visible(false); + break; + case "showmenu": + // Enable the template visibility. + $template->update_visible(true); + break; + + case 'disable': + // Disable the template visibility. + $template->update_status(false, $instance); + break; + + case 'enable': + // Disable the template visibility. + $template->update_status(true, $instance); + break; + } + + // Allow to update the changes to database. + $transaction->allow_commit(); + + // Redirect to the same page to view the templates list. + redirect($PAGE->url); +} + +$PAGE->add_body_class('mod-pulse-automation-table'); + +// Further prepare the page. +$PAGE->set_heading(get_string('autotemplates', 'pulse')); + +// Build automation templates table. +$filterset = new mod_pulse\table\automation_filterset; + +if ($categoryid = optional_param('category', null, PARAM_INT)) { + $category = new \core_table\local\filter\integer_filter('category'); + $category->add_filter_value($categoryid); + $filterset->add_filter($category); + $filtered = true; +} + +// Build automation templates table. +$table = new mod_pulse\table\auto_templates($context->id); +$table->define_baseurl($PAGE->url); +$table->set_filterset($filterset); + +// Start page output. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('autotemplates', 'pulse')); + +// Show templates description. +echo get_string('autotemplates_desc', 'pulse'); + +// Prepare 'Create template' button. +$createbutton = $OUTPUT->box_start(); +$createbutton .= mod_pulse\automation\helper::template_buttons($filtered ?? false); +$createbutton .= $OUTPUT->box_end(); + +// If there aren't any templates yet. +$countmenus = $DB->count_records('pulse_autotemplates'); +if ($countmenus < 1) { + // Show the table, which, since it is empty, falls back to the + // "There aren't any templates created yet. Please create your first template to get things going." notice. + $table->out(0, true); + + // And then show the button. + echo $createbutton; + + // Otherwise. +} else { + // Show the button. + echo $createbutton; + + // And then show the table. + $table->out(10, true); + + echo helper::get_templates_tablehelps(); +} + + +$PAGE->requires->js_amd_inline(" +require(['jquery', 'core/modal_factory', 'core/str', 'mod_pulse/modal_preset', 'mod_pulse/events'], + function($, ModalFactory, Str, ModalPreset, PresetEvents) { + + var form = document.querySelectorAll('.updatestatus-switch-form'); + form.forEach((switche) => { + switche.querySelector('.custom-switch').addEventListener('click', function(e) { + e.preventDefault(); + + var statusElem = e.target.parentNode.querySelector('input[name=action]'); + var instanceElem = e.target.parentNode.querySelector('input[name=instance]'); + + var form = e.target.closest('form'); + var checkbox = e.target.closest('.custom-control-input'); + + if (checkbox.checked) { + statusElem.value = 'enable'; + } else { + statusElem.value = 'disable'; + } + + ModalFactory.create({ + type: ModalPreset.TYPE, + title: Str.get_string('updatetemplate', 'pulse'), + body: Str.get_string('templatestatusudpate', 'pulse'), + large: true + }).then(function(modal) { + modal.setButtonText('customize', Str.get_string('updateinstance', 'pulse')); + modal.setButtonText('save', Str.get_string('updatetemplate', 'pulse')); + modal.show(); + + modal.getRoot().on(PresetEvents.customize, (e) => { + instanceElem.value = true; + form.submit(); + }); + + modal.getRoot().on(PresetEvents.save, (e) => { + instanceElem.value = false; + form.submit(); + }); + }) + }) + }); + + // Filter form display. + var filterIcon = document.querySelector('#pulse-automation-filter'); + var filterForm = document.querySelector('#pulse-automation-filterform'); + filterIcon.onclick = (e) => filterForm.classList.toggle('hide'); + +})"); + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/backup/moodle2/backup_pulse_stepslib.php b/backup/moodle2/backup_pulse_stepslib.php index 9ce2453..bc4b561 100644 --- a/backup/moodle2/backup_pulse_stepslib.php +++ b/backup/moodle2/backup_pulse_stepslib.php @@ -22,10 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die('No direct access !'); - /** - * Define the complete pulse structure for backup, with file and id annotations + * Define the complete pulse structure for backup, with file and id annotations. */ class backup_pulse_activity_structure_step extends backup_activity_structure_step { @@ -39,7 +37,8 @@ protected function define_structure() { // Define each element separated - table fields. $pulse = new backup_nested_element('pulse', array('id'), array( 'course', 'name', 'intro', 'introformat', 'pulse_subject', 'pulse_content', - 'pulse_contentformat', 'pulse', 'diff_pulse', 'resend_pulse', + 'pulse_contentformat', 'pulse', 'diff_pulse', 'displaymode', + 'boxtype', 'boxicon', 'cssclass', 'resend_pulse', 'completionavailable', 'completionself', 'completionapproval', 'completionapprovalroles', 'timemodified')); @@ -78,7 +77,7 @@ protected function define_structure() { $pulse->annotate_files('mod_pulse', 'intro', null); $pulse->annotate_files('mod_pulse', 'pulse_content', null); - $pulse = pulse_extend_backup_steps($pulse, $userinfo); + $pulse = \mod_pulse\extendpro::pulse_extend_backup_steps($pulse, $userinfo); // Return the root element (data), wrapped into standard activity structure. return $this->prepare_activity_structure($pulse); diff --git a/backup/moodle2/restore_pulse_activity_task.class.php b/backup/moodle2/restore_pulse_activity_task.class.php index 3ffee31..181f944 100644 --- a/backup/moodle2/restore_pulse_activity_task.class.php +++ b/backup/moodle2/restore_pulse_activity_task.class.php @@ -54,7 +54,7 @@ public static function define_decode_contents() { $contents[] = new restore_decode_content('pulse', array('intro', 'pulse_content'), 'pulse'); - pulse_extend_restore_content($contents); + \mod_pulse\extendpro::pulse_extend_restore_content($contents); return $contents; } diff --git a/backup/moodle2/restore_pulse_stepslib.php b/backup/moodle2/restore_pulse_stepslib.php index d6194a1..eb7f40a 100644 --- a/backup/moodle2/restore_pulse_stepslib.php +++ b/backup/moodle2/restore_pulse_stepslib.php @@ -22,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die('No direct access!'); - /** * Define all the restore steps that will be used by the restore_pulse_activity_task */ @@ -43,7 +41,7 @@ protected function define_structure() { $paths[] = new restore_path_element('pulse_users', '/activity/pulse/notifiedusers/pulse_users'); $paths[] = new restore_path_element('pulse_completion', '/activity/pulse/usercompletion/pulse_completion'); - $methods = pulse_extend_restore_structure($paths); + $methods = \mod_pulse\extendpro::pulse_extend_restore_structure($paths); // Return the paths wrapped into standard activity structure. return $this->prepare_activity_structure($paths); } @@ -133,6 +131,22 @@ protected function process_local_pulsepro_availability($data) { $newitemid = $DB->insert_record('local_pulsepro_availability', $data); } + /** + * Process pro feattures user credits table restore methods. + * Pro feature. + * @param mixed $data restore data. + * @return void + */ + protected function process_local_pulsepro_credits($data) { + global $DB; + $data = (object) $data; + $oldid = $data->id; + $data->pulseid = $this->get_new_parentid('pulse'); + $data->userid = $this->get_mappingid('user', $data->userid); + // Insert instance into Database. + $newitemid = $DB->insert_record('local_pulsepro_credits', $data); + } + /** * Update the files of editors after restore execution. * diff --git a/classes/automation/action_base.php b/classes/automation/action_base.php new file mode 100644 index 0000000..7458962 --- /dev/null +++ b/classes/automation/action_base.php @@ -0,0 +1,362 @@ +. + +/** + * Notification pulse action - Automation actions plugins controller base. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse\automation; + +use moodle_exception; + +/** + * Notification pulse action - Automation actions base. + */ +abstract class action_base { + + /** + * Repersents the actions operator is any + * @var int + */ + const OPERATOR_ANY = 1; + + /** + * Repersents the actions operator is all + * @var int + */ + const OPERATOR_ALL = 2; + + /** + * The name of this action plugin. + * + * @var string + */ + protected $component; + + /** + * Returns the shortname of the configuration. + * + * @return string The shortname of the configuration. + */ + abstract public function config_shortname(); + + /** + * Triggers the action for a specific instance and user. + * + * @param object $instancedata The data for the instance. + * @param int $userid The ID of the user. + * + * @return void + */ + abstract public function trigger_action($instancedata, $userid); + + /** + * Deletes the action associated with a template. + * + * @param int $templateid The ID of the template. + * + * @return void + */ + abstract public function delete_template_action($templateid); + + /** + * Gets the data for a specific template. + * + * @param int $templateid The ID of the template. + * + * @return mixed The data for the template. + */ + abstract public function get_data_fortemplate($templateid); + + /** + * Gets the data for a specific instance. + * + * @param int $instanceid The ID of the instance. + * + * @return mixed The data for the instance. + */ + abstract public function get_data_forinstance($instanceid); + + /** + * Loads the global form. + * + * @param object $mform The Moodle form object. + * @param mixed $forminstance The form instance. + * + * @return void + */ + abstract public function load_global_form(&$mform, $forminstance); + + /** + * Sets the action plugins name. + * + * @param string $component The component to set. + * + * @return void + */ + public function set_component(string $component) { + $this->component = $component; + } + + /** + * Gets the name of the action component. + * + * @return string The component. + */ + public function get_component() { + return $this->component; + } + + /** + * Get the instance tablename for this action. + * + * @return string Tablename. + */ + public function get_instance_tablename() { + global $DB; + return $DB->get_prefix().'pulseaction_'.$this->component.'_ins'; + } + + /** + * Gets the table name for the action component. + * + * @return string The table name for the action component. + */ + public function get_tablename() { + global $DB; + return $DB->get_prefix().'pulseaction_'.$this->component; + } + + /** + * Performs actions after data has been defined in the form. + * + * @param object $mform The Moodle form object. + * @param \automation_templates_form $forminstance The form instance. + * + * @return void + */ + public function definition_after_data(&$mform, $forminstance) { + } + + /** + * Prepares file areas for the editor. + * + * @param mixed $data The default data of the form. + * @param \context $context The context object. + * + * @return bool False. + */ + public function prepare_editor_fileareas(&$data, \context $context) { + return false; + } + + /** + * Returns the default override elements. + * + * @return array An array of default override elements. + */ + public function default_override_elements() { + return []; + } + + /** + * Observe an event triggered from conditions. + * + * @param object $instancedata The data for the instance. + * @param string $method The method to trigger. + * @param mixed $eventdata The event data. + * + * @return bool True. + */ + public function trigger_action_event($instancedata, $method, $eventdata) { + return true; + } + + /** + * Handles actions when an instance is disabled. + * + * @param int $instanceid The ID of the instance. + * @param int $status The status of the instance. + * + * @return bool True. + */ + public function instance_disabled($instanceid, $status) { + return true; + } + + /** + * Loads the instance form by calling the global form loading method. + * + * @param object $mform The automation instance form object. + * @param mixed $forminstance + * + * @return void + */ + public function load_instance_form(&$mform, $forminstance) { + $this->load_global_form($mform, $forminstance); + } + + /** + * Deletes an action instances. + * + * @param int $instanceid The ID of the instance. + * + * @return bool True. + */ + public function delete_instance_action(int $instanceid) { + global $DB; + + if (!$this->component) { + throw new moodle_exception('componentnotset', 'pulse'); + } + + $instancetable = 'pulseaction_'.$this->component.'_ins'; + return $DB->delete_records($instancetable, ['instanceid' => $instanceid]); + } + + /** + * Filters action data by its configuration shortname. + * + * @param mixed $record The record to filter. + * + * @return object The filtered record. + */ + public function filter_action_data($record) { + + $shortname = $this->config_shortname(); + + $final = helper::filter_record_byprefix($record, $shortname); + + return (object) $final; + } + + /** + * Updates encoded data. + * + * @param mixed $data The data to update. + * + * @return mixed|null + */ + public function update_encode_data(&$data) { + return null; + } + + /** + * Includes data for a template. + * + * @param object $data The data object. + * + * @return bool True if successful, false otherwise. + */ + public function include_data_fortemplate(&$data) { + global $DB; + + // In moodle, the main table should be the name of the component. + // Therefore, generate the table name based on the component name. + $actiondata = $this->get_data_fortemplate($data->templateid); + + if (empty($actiondata)) { + return false; + } + + $this->update_encode_data($actiondata); + + if (!empty($actiondata)) { + $prefix = $this->config_shortname(); + $notificationkeys = array_keys($actiondata); + array_walk($notificationkeys, function(&$value) use ($prefix) { + $value = $prefix.'_'.$value; + }); + $data = (object) array_merge((array) $data, array_combine($notificationkeys, array_values($actiondata))); + } + } + + /** + * Includes actions instance data for an instance. + * + * @param stdClass $instance The instance object. + * @param bool $addprefix Flag to add prefix to keys. + * @return stdClass|array|bool Modified instance object or data without prefix. + */ + public function include_data_forinstance(&$instance, $addprefix=true) { + + // Retrieve action data for the template. + $actiondata = $this->get_data_fortemplate($instance->templateid); + + if (empty($actiondata)) { + return false; + } + // Set the prefix based on the component. + $prefix = $this->config_shortname(); + // Get data specific to the instance. + $actioninstancedata = $this->get_data_forinstance($instance->id); + + // TODO: Get editors dynamically. + // Define editors for special handling. + $editors = ['headercontent', 'footercontent', 'staticcontent']; + // Include the override data which is used in the form. + foreach ($actioninstancedata as $configname => $configvalue) { + if ($configvalue !== null) { + $configname = in_array($configname, $editors) ? $configname.'_editor' : $configname; + $instance->override[$prefix . "_" . $configname] = 1; + } + } + // Merge instance overrides with template data. + $actiondata = \mod_pulse\automation\helper::merge_instance_overrides($actioninstancedata, $actiondata); + // Update encoded data if necessary. + $this->update_encode_data($actiondata); + // Handle dynamic content if configured. + $actiondata['mod'] = ($actiondata['dynamiccontent']) ? get_coursemodule_from_id('', $actiondata['dynamiccontent']) : []; + + // Apply prefix to keys if needed. + if (!empty($actiondata) && $addprefix) { + $notificationkeys = array_keys($actiondata); + array_walk($notificationkeys, function(&$value) use ($prefix) { + $value = $prefix.'_'.$value; + }); + + // Update the keys with prefix. + $instance = (object) array_merge((array) $instance, array_combine($notificationkeys, array_values($actiondata))); + // TODO: Include overrides. + return $instance; + } + + // Return modified instance or data without prefix. + return $actiondata; + } + + /** + * Retrieves instances associated with a specific template. + * + * @param int $templateid The ID of the template. + * + * @return array An array of template instances or an empty array if none are found. + */ + protected function get_template_instances($templateid) { + global $DB; + + if ($instances = $DB->get_records('pulse_autoinstances', ['templateid' => $templateid]) ) { + return $instances; + } + + return []; + } + +} diff --git a/classes/automation/condition_base.php b/classes/automation/condition_base.php new file mode 100644 index 0000000..2b12736 --- /dev/null +++ b/classes/automation/condition_base.php @@ -0,0 +1,226 @@ +. + +/** + * Notification pulse action - Automation conditions base. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse\automation; + +/** + * Automation conditions base. + */ +abstract class condition_base { + + /** + * Repersents the conditions operator is any. + * @var int + */ + const OPERATOR_ANY = 1; + + /** + * Repersents the conditions operator is all. + * @var int + */ + const OPERATOR_ALL = 2; + + /** + * Repersents the condition status is disabled. + * @var int + */ + const DISABLED = 0; // Option. + + /** + * Repersents the condition status is all. + * @var int + */ + const ALL = 1; + + /** + * Repersents the condition status is future. + * @var int + */ + const FUTURE = 2; + + /** + * The name of this action plugin. + * + * @var string + */ + protected $component; + + /** + * Includes an condition based to the template condition options. + * + * @param mixed $option The action option. + */ + abstract public function include_condition(&$option); + + /** + * Loads the condition form for an instance. + * + * @param moodleform $mform The form to be loaded. + * @param stdClass $forminstance The instance object. + */ + abstract public function load_instance_form(&$mform, $forminstance); + + /** + * Sets the name of the conditions component. + * + * @param string $componentname The component name. + */ + public function set_component($componentname) { + $this->component = $componentname; + } + + /** + * Adds an upcoming element to the form. + * + * @param moodleform $mform The form object. + */ + public function upcoming_element(&$mform) { + + $mform->addElement('hidden', 'condition['.$this->component.'][upcomingtime]'); + $mform->setType('condition['.$this->component.'][upcomingtime]', PARAM_INT); + } + + /** + * Gets available options. + * + * @return array List of options. + */ + public function get_options() { + return [ + self::DISABLED => get_string('disable'), + self::ALL => get_string('all'), + self::FUTURE => get_string('upcoming', 'pulse'), + ]; + } + + /** + * Triggers an actions associated to the instance. + * + * @param int $instanceid The instance ID. + * @param int $userid The user ID. + * @param mixed $expectedtime The expected time for triggering. + */ + public function trigger_instance(int $instanceid, int $userid, $expectedtime=null) { + + \mod_pulse\automation\instances::create($instanceid)->trigger_action($userid, $expectedtime); + } + + /** + * Checks if a user has completed the conditions. + * + * @param mixed $notification The notification object. + * @param int $userid The user ID. + * + * @return bool True if user has completed, otherwise false. + */ + public function is_user_completed($notification, int $userid) { + return true; + } + + /** + * Processes the saving of an instance. + * + * @param int $instanceid The instance ID. + * @param array $data The data to be saved. + * + * @return bool True if the instance was saved successfully, otherwise false. + */ + public function process_instance_save($instanceid, $data) { + global $DB; + + // Remove empty values from data array. + $filter = array_filter($data); + // If 'status' is not set and there are no other non-empty values, stopped here. + if (!isset($data['status']) && empty($filter)) { + return true; + } + + // Get the 'status' or set it to an empty string if not present. + $status = $data['status'] ?? ''; + + // Future enrolment is disabled then make the upcoming time to null. + if ($status != self::FUTURE) { + $data['upcomingtime'] = ''; + } + // Future enrolments are enabled and its upcoming is empty set current time as upcoming. + // Conditions will affect for coming enrolments after this time. + if ($status == self::FUTURE && $data['upcomingtime'] == 0) { + $data['upcomingtime'] = time(); + } + // Prepare the record to be inserted or updated. + $record = [ + 'instanceid' => $instanceid, + 'triggercondition' => $this->component, + 'status' => $data['status'] ?? null, + 'upcomingtime' => $data['upcomingtime'] ?? null, + 'isoverridden' => (isset($data['status'])) ? true : false + ]; + // Remove 'status' from data array. + unset($data['status']); + // Encode additional data as JSON. + $record['additional'] = json_encode($data); + + // Check if a record already exists for this instance and trigger condition. + if ($condition = $DB->get_record('pulse_condition_overrides', + ['instanceid' => $instanceid, 'triggercondition' => $this->component])) { + $record['id'] = $condition->id; + // Update the record. + $DB->update_record('pulse_condition_overrides', $record); + } else { + // Insert the record. + $DB->insert_record('pulse_condition_overrides', $record); + } + + return true; + } + + /** + * Includes data for an instance. + * + * @param object $instance The instance object. + * @param object $data The data object. + * @param bool $prefix Whether to add a prefix to keys or not. + * + * @return void + */ + public function include_data_forinstance(&$instance, $data, $prefix=true) { + // Decode additional data. + $additional = $data->additional ? json_decode($data->additional, true) : []; + // Add overrides to instance. + foreach ($additional as $key => $value) { + $instance->override["condition_".$this->component."_".$key] = 1; + } + + // If data is overridden, set status override. + if ($data->isoverridden) { + $instance->condition[$this->component]['status'] = $data->status; + $instance->override["condition_".$this->component."_status"] = 1; + } + // If component condition is set, merge additional data and set 'upcomingtime'. + if (isset($instance->condition[$this->component]) && $instance->condition[$this->component] != null) { + $instance->condition[$this->component] = array_merge($instance->condition[$this->component], $additional); + $instance->condition[$this->component]['upcomingtime'] = $data->upcomingtime ?: 0; + } + } +} diff --git a/classes/automation/helper.php b/classes/automation/helper.php new file mode 100644 index 0000000..97557c3 --- /dev/null +++ b/classes/automation/helper.php @@ -0,0 +1,377 @@ +. + +/** + * Notification pulse action - Automation helper. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse\automation; + +use mod_pulse\plugininfo\pulseaction; +use moodle_url; +use single_button; + +/** + * Automation helper. + */ +class helper { + + /** + * Get templates for an instance. Retrieves templates for a given course, filtering based on categories, status, and visibility. + * + * @param int|null $courseid The ID of the course. Defaults to null. + * + * @return array Associative array of templates. + */ + public static function get_templates_forinstance($courseid=null) { + global $DB; + // Get course information. + $course = get_course($courseid); + // Generate the SQL LIKE condition for categories. + $like = $DB->sql_like('categories', ':value'); + // Construct the SQL query. + $sql = "SELECT * FROM {pulse_autotemplates} + WHERE (categories = '[]' OR categories = '' OR $like) AND status = 1 AND visible = 1"; + $params = ['value' => '%"'.$course->category.'"%']; + // Retrieve records from the database. + $records = $DB->get_records_sql_menu($sql, $params); + // Format string values in the result. + array_walk($records, function(&$val) { + $val = format_string($val); + }); + + return $records; + } + + /** + * Merge instance overrides with template data. + * + * @param array $overridedata Array of overridden data. + * @param array $templatedata Array of template data. + * + * @return array Merged data. + */ + public static function merge_instance_overrides($overridedata, $templatedata) { + // Filter the empty values. + $filtered = array_filter((array) $overridedata, function($value) { + return $value !== null; + }); + // Merge the templatedata with filterdata. + $filtered = array_merge((array) $templatedata, $filtered); + + return $filtered; + } + + /** + * Filter the record data by keys with a specific prefix. + * + * @param array|object $record The record data to be filtered. + * @param string $prefix The prefix to filter keys by. + * + * @return array The filtered data with the prefix removed from keys. + */ + public static function filter_record_byprefix($record, $prefix) { + + // Filter the data based on the shortname. + $filtered = array_filter((array) $record, function($key) use ($prefix) { + return strpos($key, $prefix.'_') === 0; + }, ARRAY_FILTER_USE_KEY); + + // Remove the prefix from the keys. + $removedprefix = array_map(function($key) use ($prefix) { + return str_replace($prefix."_", '', $key); + }, array_keys($filtered)); + + // Combine the filtered values with prefix removed keys. + $final = array_combine(array_values($removedprefix), array_values($filtered)); + + return $final; + } + + /** + * Get a list of available actions plugins. + * + * @return array An array of available actions. + */ + public static function get_actions() { + return \mod_pulse\plugininfo\pulseaction::get_list(); + } + + /** + * Get a list of available conditions. + * + * @return array An array of available conditions. + */ + public static function get_conditions() { + return \mod_pulse\plugininfo\pulsecondition::get_list(); + } + + /** + * Prepare editor draft files for actions. + * + * @param array $data The data to be prepared. + * @param context $context The context. + */ + public static function prepare_editor_draftfiles(&$data, $context) { + + $actions = self::get_actions(); + foreach ($actions as $key => $action) { + $action->prepare_editor_fileareas($data, $context); + } + } + + /** + * Post-update editor draft files for actions. + * + * @param array $data The data to be updated. + * @param context $context The context. + */ + public static function postupdate_editor_draftfiles(&$data, $context) { + + $actions = self::get_actions(); + foreach ($actions as $key => $action) { + $action->postupdate_editor_fileareas($data, $context); + } + } + + /** + * Get instances associated with a course. + * + * @param int $courseid The ID of the course. + * + * @return array An array of instances associated with the course. + */ + public static function get_course_instances($courseid) { + global $DB; + + $list = $DB->get_records('pulse_autoinstances', ['courseid' => $courseid]); + + return $list; + } + + /** + * Insert the additional module fields data to the table. + * + * @param int $tablename + * @param int $instanceid + * @param array $options + * @return void + */ + public static function update_instance_option($tablename, int $instanceid, $options) { + global $DB; + + $records = []; + + foreach ($options as $name => $value) { + + if ($DB->record_exists($tablename, ['instanceid' => $instanceid, 'name' => $name])) { + $DB->set_field($tablename, 'value', $value, ['instanceid' => $instanceid, 'name' => $name]); + } else { + $record = new \stdClass; + $record->instanceid = $instanceid; + $record->name = $name; + $record->value = $value ?: ''; + $record->isoverridden = true; // Update overridden. + // Store to the list then insert at once after all the creations. + $records[$name] = $record; + } + } + + if (isset($records) && !empty($records)) { + $DB->insert_records($tablename, $records); + } + } + + /** + * Generate the button which is displayed on top of the templates table. Helps to create templates. + * + * @param bool $filtered Is the table result is filtered. + * @return string The HTML contents to display the create templates button. + */ + public static function template_buttons($filtered=false) { + global $OUTPUT, $DB, $CFG; + + require_once($CFG->dirroot. '/mod/pulse/automation/automationlib.php'); + + // Setup create template button on page. + $caption = get_string('templatecreatenew', 'pulse'); + $editurl = new moodle_url('/mod/pulse/automation/templates/edit.php', ['sesskey' => sesskey()]); + + // IN Moodle 4.2, primary button param depreceted. + $primary = defined('single_button::BUTTON_PRIMARY') ? single_button::BUTTON_PRIMARY : true; + $button = new single_button($editurl, $caption, 'get', $primary); + $button = $OUTPUT->render($button); + + // Filter form. + $button .= \html_writer::start_div('filter-form-container'); + $button .= \html_writer::link('javascript:void(0)', $OUTPUT->pix_icon('i/filter', 'Filter'), [ + 'id' => 'pulse-automation-filter', + 'class' => 'sort-autotemplates btn btn-primary ml-2 ' . ($filtered ? 'filtered' : '') + ]); + $filter = new \template_table_filter(); + $button .= \html_writer::tag('div', $filter->render(), ['id' => 'pulse-automation-filterform', 'class' => 'hide']); + $button .= \html_writer::end_div(); + + // Sort button for the table. Sort y the reference. + $tdir = optional_param('tdir', null, PARAM_INT); + $tdir = ($tdir == SORT_ASC) ? SORT_DESC : SORT_ASC; + $dirimage = ($tdir == SORT_ASC) ? '' : $OUTPUT->pix_icon('t/sort_by', 'Sortby'); + + $manageurl = new moodle_url('/mod/pulse/automation/templates/list.php', [ + 'tsort' => 'reference', 'tdir' => $tdir + ]); + $tempcount = $DB->count_records('pulse_autotemplates'); + if (!empty($tempcount)) { + $button .= \html_writer::link($manageurl->out(false), $dirimage.get_string('sort'), [ + 'class' => 'sort-autotemplates btn btn-primary ml-2' + ]); + } + + return $button; + } + + /** + * Create instance from templates. + * + * @param int $courseid + * @return string Form with templates list and manage templates button. + */ + public static function get_addtemplate_instance($courseid) { + global $OUTPUT, $CFG; + + require_once($CFG->dirroot. '/mod/pulse/automation/automationlib.php'); + + // Form to add automation template as instance for the course. + $url = (new moodle_url('/mod/pulse/automation/instances/edit.php', ['course' => $courseid]))->out(false); + $form = new \template_addinstance_form($url, ['courseid' => $courseid], 'get'); + + $html = \html_writer::start_tag('div', ['class' => 'template-add-form']); + $templates = self::get_templates_forinstance($courseid); + if (!empty($templates)) { + $html .= $form->render(); + } + + // Button to access the manage the automation templates list. + $manageurl = new moodle_url('/mod/pulse/automation/templates/list.php'); + $html .= \html_writer::link($manageurl->out(true), + get_string('managetemplate', 'pulse'), ['class' => 'btn btn-primary', 'target' => '_blank']); + + $tdir = optional_param('tdir', null, PARAM_INT); + $tdir = ($tdir == SORT_ASC) ? SORT_DESC : SORT_ASC; + $dirimage = ($tdir == SORT_ASC) ? '' : $OUTPUT->pix_icon('t/sort_by', 'Sortby'); + + $manageurl = new moodle_url('/mod/pulse/automation/instances/list.php', [ + 'courseid' => $courseid, 'tsort' => 'idnumber', 'tdir' => $tdir + ]); + if (!empty($templates)) { + $html .= \html_writer::link($manageurl->out(false), + $dirimage.get_string('sort'), ['class' => 'sort-autotemplates btn btn-primary ml-2']); + } + $html .= \html_writer::end_tag('div'); + + return $html; + } + + /** + * Templates table helps content. + * + * @return string + */ + public static function get_templates_tablehelps() { + global $OUTPUT; + + $actions = \mod_pulse\plugininfo\pulseaction::instance()->get_plugins_base(); + array_walk($actions, function(&$value) { + $classname = 'pulseaction_'.$value->get_component(); + $result['badge'] = \html_writer::tag('span', + get_string('formtab', 'pulseaction_'.$value->get_component()), ['class' => 'badge badge-primary '.$classname]); + $result['icon'] = \html_writer::span($value->get_action_icon(), 'action', ['class' => 'action-icon '.$classname]); + $value = $result; + }); + + $templatehelp = [ + 'help1' => implode(',', array_column($actions, 'icon')), + 'help2' => get_string('automationwelcomemsg', 'pulse'), + 'help3' => implode(',', array_column($actions, 'badge')), + 'help4' => '
'.get_string('automationreferencedemo', 'pulse').'
', + 'help5' => $OUTPUT->pix_icon('t/edit', \get_string('edit')), + 'help6' => $OUTPUT->pix_icon('t/hide', \get_string('hide')), + 'help7' => \html_writer::tag('div', ' + ', ['class' => "custom-control custom-switch"]), + 'help8' => \html_writer::tag('label', "33 (1)", ['class' => 'overrides badge badge-secondary pl-10']), + ]; + + $table = new \html_table(); + $table->id = 'plugins-control-panel'; + $table->head = array('', ''); + + foreach ($templatehelp as $help => $result) { + $row = new \html_table_row(array($result, get_string('automationtemplate_'.$help, 'pulse'))); + $table->data[] = $row; + } + + $html = \html_writer::tag('h3', get_string('instruction', 'pulse')); + $html .= \html_writer::table($table); + return \html_writer::tag('div', $html, ['class' => 'automation-instruction']); + } + + /** + * Get instance table instructions helps. + * + * @return void + */ + public static function get_instance_tablehelps() { + global $OUTPUT; + + $actions = \mod_pulse\plugininfo\pulseaction::instance()->get_plugins_base(); + array_walk($actions, function(&$value) { + $classname = 'pulseaction_'.$value->get_component(); + $result['badge'] = \html_writer::tag('span', + get_string('formtab', 'pulseaction_'.$value->get_component()), ['class' => 'badge badge-primary ' . $classname]); + $result['icon'] = \html_writer::span($value->get_action_icon(), 'action', ['class' => 'action-icon ' . $classname]); + $value = $result; + }); + + $templatehelp = [ + 'help1' => implode(',', array_column($actions, 'icon')), + 'help2' => get_string('automationwelcomemsg', 'pulse'), + 'help3' => implode(',', array_column($actions, 'badge')), + 'help4' => '
'.get_string('automationreferencedemo', 'pulse').'
', + 'help5' => $OUTPUT->pix_icon('t/edit', \get_string('edit')), + 'help6' => $OUTPUT->pix_icon('t/copy', \get_string('copy')), + 'help7' => $OUTPUT->pix_icon('i/calendar', \get_string('copy')), + 'help8' => $OUTPUT->pix_icon('t/hide', \get_string('hide')), + ]; + + $table = new \html_table(); + $table->id = 'plugins-control-panel'; + $table->head = array('', ''); + + foreach ($templatehelp as $help => $result) { + $row = new \html_table_row(array($result, get_string('automationinstance_'.$help, 'pulse'))); + $table->data[] = $row; + } + + $html = \html_writer::tag('h3', get_string('instruction', 'pulse')); + $html .= \html_writer::table($table); + return \html_writer::tag('div', $html, ['class' => 'automation-instruction']); + } + +} diff --git a/classes/automation/instances.php b/classes/automation/instances.php new file mode 100644 index 0000000..8406d76 --- /dev/null +++ b/classes/automation/instances.php @@ -0,0 +1,655 @@ +. + +/** + * Notification pulse action - Automation instances. + * + * This controller handles the manitence of automation instance create, edit and delete. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_pulse\automation; + +use availability_completion\condition; +use \mod_pulse\automation\action_base; +use moodle_url; +use core_reportbuilder\local\helpers\report as reporthelper; +use course_enrolment_manager; +use moodle_exception; + +/** + * Automation Instance controller, handles instance data management. + */ +class instances extends templates { + + /** + * The ID of the automation instance. + * + * @var int + */ + protected $instanceid; + + /** + * The processed data of the automation instance. + * + * @var stdclass + */ + protected $instance; + + /** + * List of pulse action plugins + * + * @var stdclass + */ + protected $actions; + + /** + * Constructor for the class. + * + * @param int $instanceid The ID of the instance. + * + * @throws moodle_exception If the instance does not exist. + */ + public function __construct($instanceid) { + // TODO Check istance exists. throw exception if not available. + $this->instanceid = $instanceid; + $this->set_instance_actions(); + } + + /** + * Create a new instance of the class. + * + * @param int $instanceid The ID of the instance. + * + * @return self An instance of this class. + */ + public static function create($instanceid) { + $instance = new self($instanceid); + return $instance; + } + + /** + * Get instance data. Contains the conditions, and actions data related to this instance. + * Those data's are generated based on overrides. + * + * @return stdclass + */ + public function get_instance_data() { + + // Fetch the instance record to find the template id. + $instance = $this->get_instance_record(); + + if (empty($instance)) { + throw new moodle_exception('instancedatanotgenerated', 'pulse'); + } + // Include the template data with overrides merged. + $instance->template = \mod_pulse\automation\templates::create($instance->templateid)->get_data_forinstance($instance); + // Include the actions plugins data. + $instance->actions = $this->include_actions_data($instance, false); + // Include all the conditions data. + $this->include_conditions_data($instance); + + $this->instance = $instance; + // Include the course information to instance data. + $this->instance->course = get_course($instance->courseid); + + return $this->instance; + } + + /** + * Get the record for the current instance. + * + * @return stdClass|false The instance record or false if not found. + */ + protected function get_instance_record() { + global $DB; + + return $DB->get_record('pulse_autoinstances', ['id' => $this->instanceid]); + } + + /** + * Get the instance formdata. Contains the override info, these data is set to the form. + * + * Actions data are included with its config prefix. + * + * @return array + */ + public function get_instance_formdata() { + + // Fetch the instance record to find the template id. + $instance = $this->get_instance_record(); + + if (empty($instance)) { + throw new moodle_exception('instancedatanotgenerated', 'pulse'); + } + + // Fetch record of template instance. + \mod_pulse\automation\templates::create($instance->templateid)->get_data_forinstance($instance); + + $this->include_actions_data($instance); + + $this->include_conditions_data($instance); + + return ((array) $instance); + } + + /** + * Include conditions data for the given instance. + * + * @param stdClass $instance The instance object. + * + * @return array The array of conditions data. + */ + public function include_conditions_data(&$instance) { + global $DB; + + $overrides = $DB->get_records('pulse_condition_overrides', ['instanceid' => $instance->id]); + $conditions = \mod_pulse\plugininfo\pulsecondition::get_list(); + + // Define empty list of conditions. + if (!isset($instance->condition)) { + $instance->condition = []; + } + + // Include template conditions. + + $triggerconditions = $instance->template->triggerconditions ?? $instance->triggerconditions; + array_map(function($value) use ($instance, $conditions) { + $instance->condition[$value] = ['status' => 1]; + }, $triggerconditions); + + // Override the instance conditions to template conditions. + foreach ($overrides as $condition) { + if (isset($conditions[$condition->triggercondition])) { + $triggercon = $condition->triggercondition; + $conditiondata[$triggercon] = $conditions[$triggercon]->include_data_forinstance($instance, $condition); + } + } + + return $conditiondata ?? []; + } + + /** + * Include actions data for the given instance. + * + * @param stdClass $instance The instance object. + * @param bool $prefix Whether to include prefixes. + * + * @return array The array of actions data. + */ + public function include_actions_data(&$instance, $prefix=true) { + + // Fetch the list of enabled action plugins. + $actionplugins = $this->actions; + foreach ($actionplugins as $name => $plugin) { + // Include all the actions data for this template. + $actiondata[$name] = $plugin->include_data_forinstance($instance, $prefix); + } + + return $actiondata; + } + + /** + * Get the course ID associated with this instance. + * + * @return int The course ID. + */ + public function get_courseid() { + global $DB; + return $DB->get_field('pulse_autoinstances', 'courseid', ['instance' => $this->instanceid]); + } + + /** + * Set the instance actions based on enable status. + * TODO: Fetch the actions based on the enable status for instances in future. + */ + public function set_instance_actions() { + // TODO: Fetch the actions based on the enable status for instances in future. + $this->actions = \mod_pulse\plugininfo\pulseaction::get_list(); + } + + /** + * Updates the "visible" field of the current menu and deletes it from the cache. + * + * @param bool $status The new value for the "status" field. + * @param bool $instance + * @return bool True if the update was successful, false otherwise. + */ + public function update_status(bool $status, bool $instance = false) { + + foreach ($this->actions as $component => $action) { + $action->instance_disabled($this->instanceid, $status); + } + + return $this->update_field('status', $status, ['id' => $this->instanceid]); + } + + /** + * Updates a field of the current menu with the given key and value. + * + * @param string $key The key of the field to update. + * @param mixed $value The new value of the field. + * @return bool|int Returns true on success, or false on failure. it also deletes the current menu from cache. + */ + public function update_field($key, $value) { + global $DB; + + $result = $DB->set_field('pulse_autoinstances', $key, $value, ['id' => $this->instanceid]); + + return $result; + } + + /** + * Delete the current menu and all its associated items from the database. + * + * @return bool True if the deletion is successful, false otherwise. + */ + public function delete_instance() { + global $DB; + + if ($DB->delete_records('pulse_autoinstances', ['id' => $this->instanceid])) { + // Delete all of its instance data from templates. + $DB->delete_records('pulse_autotemplates_ins', ['instanceid' => $this->instanceid]); + // Delete all its actions data for this instance. + $this->delete_actions_instances($this->instanceid); + + return true; + } + return false; + } + + /** + * Create a duplicate of this instance. Fetch the formatdata for this instance and remove the ids. + * Then send to the manage-instance method it will create new instance. + * + * @return void + */ + public function duplicate() { + + $instance = (object) $this->get_instance_formdata(); + $instance->id = 0; + $instance->instanceid = 0; + + foreach (helper::get_actions() as $action) { + $config = $action->config_shortname(); + if (isset($instance->{$config.'_id'})) { + $instance->{$config.'_id'} = 0; + } + } + + $context = \context_system::instance(); + helper::prepare_editor_draftfiles($instance, $context); + + // Create the item. + self::manage_instance($instance); + } + + /** + * Get the URL to view the notification report. + * + * @return moodle_url The URL to the report. + */ + public function get_report_url() { + global $DB; + + $data = [ + 'source' => 'pulseaction_notification\reportbuilder\datasource\notification', + 'component' => 'pulseaction_notification' + ]; + if ($report = $DB->get_record('reportbuilder_report', $data)) { + $url = new moodle_url('/reportbuilder/view.php', ['id' => $report->id, 'instanceid' => $this->instanceid]); + } else { + $data['name'] = get_string('automationreportname', 'pulse'); + $instance = reporthelper::create_report((object) $data, (bool) 1); + $id = $instance->get('id'); + $url = new moodle_url('/reportbuilder/view.php', ['id' => $id, 'instanceid' => $this->instanceid]); + } + + return $url; + } + + /** + * Delete all the available actions linked with this template. + * + * Find the lis of actions and get linked template instance based template id and delete those actions. + * + * @param int $instanceid + * @return void + */ + public function delete_actions_instances($instanceid) { + global $DB; + // Fetch the list of enabled action plugins. + $actionplugins = \mod_pulse\plugininfo\pulseaction::get_list(); + foreach ($actionplugins as $name => $plugin) { + // Delete the instance data related to the action. + $plugin->delete_instance_action($instanceid); + } + } + + /** + * Trigger the action for a user. + * + * @param int $userid The ID of the user. + * @param int|null $runtime The runtime of the action. + * @param bool $newuser Whether this is a new user. + */ + public function trigger_action($userid, $runtime=null, $newuser=false) { + global $DB; + + // Check the trigger conditions are ok. + $instancedata = (object) $this->get_instance_formdata(); + + foreach ($this->actions as $name => $plugin) { + // Send the trigger conditions are statified, then initate the instances based. + $plugin->trigger_action($instancedata, $userid, $runtime, $newuser); + } + } + + /** + * Trigger an action event. + * + * @param string $method The method to trigger. + * @param mixed $eventdata The event data. + */ + public function trigger_action_event($method, $eventdata) { + global $DB; + + // Check the trigger conditions are ok. + $instancedata = (object) $this->get_instance_data(); + + foreach ($this->actions as $name => $plugin) { + // Send the trigger conditions are statified, then initate the instances based. + $plugin->trigger_action_event($instancedata, $method, $eventdata); + } + + } + + /** + * Find the user completion conditions. + * + * @param stdclass $conditions + * @param stdclass $instancedata + * @param int $userid + * @param bool $isnewuser + * @return void + */ + public function find_user_completion_conditions($conditions, $instancedata, $userid, $isnewuser=false) { + global $CFG; + + require_once($CFG->dirroot.'/lib/completionlib.php'); + // Get course completion info instance. + + $course = $instancedata->course ?? get_course($instancedata->courseid); + + $completion = new \completion_info($course); + + // Trigger condition operator method, require the user to complete all the conditions or any of one is fine. + $count = ($instancedata->triggeroperator == action_base::OPERATOR_ALL); + $enabled = $result = 0; + + foreach ($conditions as $component => $option) { + // Get the condition plugin instance. + $condition = \mod_pulse\plugininfo\pulsecondition::instance()->get_plugin($component); + // Status of the condition, some conditions have additional values. + $status = (is_array($option)) ? $option['status'] : $option; + // No need to check the condition if condition is set as future enrolment and the user is old user. + if ($status <= 0 ) { + continue; + } + // Condition is only configured to verify the future enrolment. + if ($status == condition_base::FUTURE && !$isnewuser) { + $userenroltime = $this->get_user_enrolment_createtime($userid, $instancedata->course); + // User enrolled before the condition is set as upcoming. then not need to verify the condition. + // User is passed this condition by default. + if ($userenroltime < $option['upcomingtime']) { + continue; + } + } + + $enabled++; // Increase enabled condition count. + + if ($condition->is_user_completed($instancedata, $userid, $completion)) { + + $result++; // Increase completed condition count for this user. + // Instance only configured to complete any one of the conditions. + if (!$count) { + return true; // Break the loop, found one completed condition. + } + } + } + + return ($enabled == $result) ? true : false; + } + + /** + * Get the creation time of a user's enrolment in a course. + * + * @param int $userid The ID of the user. + * @param stdClass $course The course object. + * @return int|false The enrolment creation time or false if not found. + */ + public function get_user_enrolment_createtime($userid, $course) { + global $PAGE, $CFG; + + require_once($CFG->dirroot.'/enrol/locallib.php'); + + static $context; + static $courseid; + + if ($context == null || $course != $course->id) { + $context = \context_course::instance($course->id); + $courseid = $course->id; + } + $enrolments = (new course_enrolment_manager($PAGE, $course))->get_user_enrolments($userid); + + if (!empty($enrolments)) { + $enrolmenttime = current($enrolments)->timecreated; + return $enrolmenttime; + } + + return false; + } + + /** + * Insert or update the menu instance to DB. Convert the multiple options select elements to json. + * setup menu order after insert. + * + * Delete the current menu cache after updated the menu. + * + * @param stdclass $formdata + * @return bool + */ + public static function manage_instance($formdata) { + global $DB; + + $record = $formdata; + + // Filter the overridden enabled form elements names as a list. + $override = $record->override; + + if ($override = $record->override) { + + array_walk($override, function($value, $key) use (&$override) { + $length = strlen('_editor'); + if (substr_compare($key, '_editor', -$length) === 0) { // Find elements Ends with _editor. + $key = str_replace('_editor', '', $key); + $override[$key] = $value; + } + + // Update the interval key to notify. + // TODO: Update the method to notification action. + // TODO: create hook to update the elements or add override element for groups. + }); + + $overridenkeys = array_filter($override, function($value) { + return $value ? true : false; + }); + + // Fetch the list of keys need to remove override values - not overrides. + $removeoverridenkeys = array_filter($override, function($value) { + return $value ? false : true; + }); + + if (isset($formdata->instanceid)) { + self::remove_override_values($removeoverridenkeys, $formdata->instanceid, $formdata->templateid); + } + } + + // Start the database transcation. + $transaction = $DB->start_delegated_transaction(); + + // Instance data to store in autoinstance table. + $instancedata = (object) [ + 'templateid' => $formdata->templateid, + 'courseid' => $formdata->courseid, + 'status' => true, + ]; + + // Check the isntance is already created. if created update the record otherwise create new instance. + + if (isset($formdata->instanceid) && $DB->record_exists('pulse_autoinstances', ['id' => $formdata->instanceid])) { + + $instancedata->id = $formdata->instanceid; + // Update the template. + $DB->update_record('pulse_autoinstances', $instancedata); + // Show the edited success notification. + \core\notification::success(get_string('templateupdatesuccess', 'pulse')); + + $instanceid = $instancedata->id; + + } else { + $instanceid = $DB->insert_record('pulse_autoinstances', $instancedata); + // Show the inserted success notification. + \core\notification::success(get_string('templateinsertsuccess', 'pulse')); + } + + // Store the tags. + if (isset($overridenkeys['tags']) && !empty($overridenkeys['tags'])) { + $tagoptions = self::get_tag_instance_options(); + $context = \context_system::instance(); + if (!empty($record->tags)) { + \core_tag_tag::set_item_tags( + $tagoptions['component'], $tagoptions['itemtype'], $instanceid, $context, $record->tags); + } + } + // Store the templates, conditions and actions data. Find the overridden elements. + + $conditions = helper::filter_record_byprefix($override, 'condition'); + + foreach ($conditions as $key => $status) { + $component = explode('_', $key)[0]; + if (!isset($record->condition[$component])) { + continue; + } + $conditionname = "pulsecondition_".$component."\conditionform"; + $condition = new $conditionname(); + $condition->set_component($component); + + $condition->process_instance_save($instanceid, $record->condition[$component]); + } + + // Fetch the value of the overridden settings. + $overriddenelements = array_intersect_key((array) $record, $overridenkeys); + + // ...Store the templates overrides. + // Fetch the auto templates fields. + $templatefields = $DB->get_columns('pulse_autotemplates_ins'); + $fields = array_keys($templatefields); + $preventfields = ['id', 'triggerconditions', 'timemodified']; + + // Clear unused fields from list. + $fields = array_diff_key(array_flip($fields), array_flip($preventfields)); + $templatedata = array_intersect_key((array) $overriddenelements, $fields); + + // Convert the elements array into json. + array_walk($templatedata, function(&$value) { + if (is_array($value)) { + $value = json_encode($value); + } + }); + + $tablename = 'pulse_autotemplates_ins'; // Template instance tablename to update. + // Update the instance overridden data related to template. + \mod_pulse\automation\templates::update_instance_data($instanceid, $templatedata); + + // ...Send the data to action plugins for perform the data store. + $context = \context_course::instance($record->courseid); + // Find list of actions. + $actionplugins = \mod_pulse\plugininfo\pulseaction::get_list(); + + // Added the item id for file editors. + $overriddenelements['instanceid'] = $instanceid; + $overriddenelements['courseid'] = $record->courseid; + $overriddenelements['templateid'] = $formdata->templateid; + + foreach ($actionplugins as $component => $pluginbase) { + $pluginbase->postupdate_editor_fileareas($overriddenelements, $context); + $pluginbase->process_instance_save($instanceid, $overriddenelements); + } + + // Allow to update the DB changes to Database. + $transaction->allow_commit(); + + return $instanceid; + } + + /** + * Remove the values of previously overrides values, those values are removed now. + * + * @param array $fields + * @param int $instanceid + * @return void + */ + protected static function remove_override_values($fields, $instanceid) { + global $DB; + + if (!empty($fields)) { + // Remove the conditions overrides. + $conditions = helper::filter_record_byprefix($fields, 'condition'); + foreach ($conditions as $key => $status) { + $component = explode('_', $key)[0]; + $DB->set_field('pulse_condition_overrides', 'isoverridden', null, + ['instanceid' => $instanceid, 'triggercondition' => $component]); + } + + // Remove template fields data. + $templatefields = $DB->get_columns('pulse_autotemplates'); + $tempfields = array_keys($templatefields); + $preventfields = ['id', 'triggerconditions', 'timemodified']; + // Clear unused fields from list. + $tempfields = array_diff_key(array_flip($tempfields), array_flip($preventfields)); + $templatedata = array_intersect_key((array) $fields, $tempfields); + $templatedata = array_fill_keys(array_keys($templatedata), null); + $templatedata['id'] = $DB->get_field('pulse_autotemplates_ins', 'id', ['instanceid' => $instanceid]); + $DB->update_record('pulse_autotemplates_ins', $templatedata); + + // Remove the actions overrides. + $actions = helper::get_actions(); + foreach ($actions as $component => $pluginbase) { + // Filter the current action data from the templates data by its shortname. + $actiondata = $pluginbase->filter_action_data((array) $fields); + $actiondata = (object) array_fill_keys(array_keys((array) $actiondata), null); + if (empty((array) $actiondata)) { + continue; + } + $actiondata->id = $DB->get_field('pulseaction_'.$component.'_ins', 'id', ['instanceid' => $instanceid]); + $DB->update_record('pulseaction_'.$component.'_ins', $actiondata); + } + } + } +} diff --git a/classes/automation/templates.php b/classes/automation/templates.php new file mode 100644 index 0000000..7011a6e --- /dev/null +++ b/classes/automation/templates.php @@ -0,0 +1,475 @@ +. + +/** + * Notification pulse action - Manage automation templates. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_pulse\automation; + +use context_system; +use context_module; +use core_tag_tag; + +/** + * Manage automation templates. + */ +class templates { + + /** + * Repersents the template visibility is shown. + * @var int + */ + const VISIBILITY_SHOW = 1; + + /** + * Repersents the template visibility is hidden. + * @var int + */ + const VISIBILITY_HIDDEN = 0; + + /** + * Repersents the template status is enabled. + * @var int + */ + const STATUS_ENABLE = 1; + + /** + * Repersents the template status is disabled. + * @var int + */ + const STATUS_DISABLE = 0; + + /** + * ID of the automation template. + * + * @var int + */ + protected $templateid; // Template id. + + /** + * The record of the templates + * + * @var stdclass + */ + protected $template; + + /** + * Constructor for initializing a template. + * + * @param int $templateid The ID of the template. + */ + protected function __construct($templateid) { + $this->templateid = $templateid; + $this->template = $this->get_template_record(); + } + + /** + * Get the template associated with this instance. + * + * @return stdClass The template object. + */ + public function get_template() { + return $this->template; + } + + /** + * Create an instance of the template. + * + * @param int $templateid The ID of the template. + * @return self The instance of the template. + */ + public static function create($templateid) { + // TODO: template exist checks goes here. + return new self($templateid); + } + + /** + * Retrieve the template record from the database. + * + * @return stdClass The template record. + */ + protected function get_template_record() { + + $data = $this->get_formdata(); // Fetch the template data from DB. + + // Convert the json values to array. + $data->tenants = json_decode($data->tenants); + $data->categories = json_decode($data->categories); + $data->triggerconditions = json_decode($data->triggerconditions); + + // Get the tags for the template. + $tagoptions = self::get_tag_options(); + $data->tags = core_tag_tag::get_item_tags_array($tagoptions['component'], $tagoptions['itemtype'], $data->id); + + $data->templateid = $data->id; + // Include all available actions data for this current template. + $this->include_actions_data($data); + + return $data; + } + + /** + * Get form data for a template. + * + * @return stdClass The template data. + */ + public function get_formdata() { + global $DB; + + if ($autotemplate = $DB->get_record('pulse_autotemplates', ['id' => $this->templateid], '*', MUST_EXIST)) { + return $autotemplate; + } + } + + /** + * Get data for a specific instance. + * + * @param stdClass $instance The instance object. + * @return stdClass The instance data. + */ + public function get_data_forinstance(&$instance) { + global $DB; + + $autotemplateins = $DB->get_record('pulse_autotemplates_ins', ['instanceid' => $instance->id], '*', MUST_EXIST); + $autotemplate = $this->get_formdata(); + + $overridedata = array_filter((array) $autotemplateins); + foreach ($overridedata as $key => $value) { + if (!in_array($key, ['id', 'timemodified', 'instanceid'])) { + $instance->override[$key] = 1; + } + } + + // Merge the override data intop of template data. + $data = (object) \mod_pulse\automation\helper::merge_instance_overrides($autotemplateins, $autotemplate); + + // Convert the json values to array. + $data->tenants = json_decode($data->tenants); + $data->categories = json_decode($data->categories); + $data->triggerconditions = json_decode($data->triggerconditions); + $data->tags = json_decode($data->tags); + + $data->templateid = $autotemplate->id; + unset($data->id); // Remove the template id. + + $instance = (object) array_merge((array) $instance, (array) $data); + + return $data; + } + + /** + * Get raw data for templates. NOT RECOMMENDED + * + * @return stdClass The raw template data. + */ + public function get_templates_rawdata() { + global $DB; + + $actions = \mod_pulse\plugininfo\pulseaction::get_list(); + + $select = ['te.id as templateid']; + $join = []; + $i = 0; + foreach ($actions as $action) { + $i++; + $tablename = $action->get_tablename(); + if (!$tablename) { + continue; + } + $sht = 'ac'.$i; + $join[] = " JOIN $tablename AS $sht ON $sht.templateid=te.id "; + $select[] = "$sht.id as $sht, $sht.*"; + } + $select[] = 'te.*'; + $select = implode(', ', $select); + $join = implode(' ', $join); + + $sql = "SELECT $select FROM {pulse_autotemplates} te"; + $sql .= $join; + $sql .= " WHERE te.id=:templateid"; + + return $DB->get_record_sql($sql, ['templateid' => $this->templateid]); + } + + /** + * List of instanced created using this template + * + * @return void + */ + public function get_instances() { + global $DB; + + $instances = $DB->get_records('pulse_autoinstances', ['templateid' => $this->templateid]); + $overrides = []; + $overinstances = []; + if (!empty($instances)) { + foreach ($instances as $instanceid => $instance) { + $insobj = new \mod_pulse\automation\instances($instance->id); + $formdata = (object) $insobj->get_instance_formdata(); + foreach ($formdata->override as $key => $value) { + if (isset($overrides[$key])) { + $overrides[$key] += 1; + $overinstances[$key][] = ['id' => $instance->id, 'name' => format_string($formdata->title)]; + } else { + $overrides[$key] = 1; + $overinstances[$key] = [['id' => $instance->id, 'name' => format_string($formdata->title)]]; + } + } + } + } + + return [$overrides, $overinstances]; + } + + /** + * Include actions data. + * + * @param stdclass $data + * @return void + */ + public function include_actions_data(&$data) { + + // Fetch the list of enabled action plugins. + $actionplugins = \mod_pulse\plugininfo\pulseaction::get_list(); + foreach ($actionplugins as $name => $plugin) { + // Include all the actions data for this template. + $plugin->include_data_fortemplate($data); + } + } + + /** + * Delete all the available actions linked with this template. + * + * Find the lis of actions and get linked template instance based template id and delete those actions. + * + * @param int $templateid + * @return void + */ + public function delete_template_actions($templateid) { + global $DB; + // Fetch the list of enabled action plugins. + $actionplugins = \mod_pulse\plugininfo\pulseaction::get_list(); + foreach ($actionplugins as $name => $plugin) { + $plugin->delete_template_action($templateid); + } + } + + /** + * Updates the "visible" field of the current menu and deletes it from the cache. + * + * @param bool $visible The new value for the "visible" field. + * @return bool True if the update was successful, false otherwise. + */ + public function update_visible(bool $visible) { + + return $this->update_field('visible', $visible, ['id' => $this->templateid]); + } + + /** + * Updates the "visible" field of the current menu and deletes it from the cache. + * + * @param bool $status The new value for the "status" field. + * @param bool $instance + * @return bool True if the update was successful, false otherwise. + */ + public function update_status(bool $status, bool $instance=false) { + global $DB; + + if ($instance) { + $DB->set_field('pulse_autoinstances', 'status', $status, ['templateid' => $this->templateid]); + } + + return $this->update_field('status', $status, ['id' => $this->templateid]); + } + + /** + * Updates a field of the current menu with the given key and value. + * + * @param string $key The key of the field to update. + * @param mixed $value The new value of the field. + * @return bool|int Returns true on success, or false on failure. it also deletes the current menu from cache. + */ + public function update_field($key, $value) { + global $DB; + + $result = $DB->set_field('pulse_autotemplates', $key, $value, ['id' => $this->templateid]); + + return $result; + } + + /** + * Delete the current menu and all its associated items from the database. + * + * @return bool True if the deletion is successful, false otherwise. + */ + public function delete_template() { + global $DB; + if ($DB->delete_records('pulse_autotemplates', ['id' => $this->templateid])) { + // Delete all its actions. + $this->delete_template_actions($this->templateid); + + return true; + } + return false; + } + + /** + * Get options for tagging templates. + * + * @return array An associative array with 'itemtype' and 'component'. + */ + public static function get_tag_options() { + return [ + 'itemtype' => 'pulse_autotemplates', + 'component' => 'mod_pulse' + ]; + } + + /** + * Get options for tagging template instances. + * + * @return array An associative array with 'itemtype' and 'component'. + */ + public static function get_tag_instance_options() { + return [ + 'itemtype' => 'pulse_autotemplates_ins', + 'component' => 'mod_pulse' + ]; + } + + + /** + * Insert or update the menu instance to DB. Convert the multiple options select elements to json. + * setup menu order after insert. + * + * Delete the current menu cache after updated the menu. + * + * @param stdclass $formdata + * @return bool + */ + public static function manage_instance($formdata) { + global $DB; + + $record = clone $formdata; + + // Encode the multiple value elements into json to store. + foreach ($record as $key => $value) { + if (is_array($value)) { + $record->$key = json_encode($value); + } + } + + $transaction = $DB->start_delegated_transaction(); + + // Create template record. + $record->reference = shorten_text(strip_tags($record->reference), 30); + if (isset($formdata->id) && $DB->record_exists('pulse_autotemplates', ['id' => $formdata->id])) { + $templateid = $formdata->id; + // Update the template. + $DB->update_record('pulse_autotemplates', $record); + + // Show the edited success notification. + \core\notification::success(get_string('templateupdatesuccess', 'pulse')); + } else { + $templateid = $DB->insert_record('pulse_autotemplates', $record); + // Show the inserted success notification. + \core\notification::success(get_string('templateinsertsuccess', 'pulse')); + } + + // Update template tags. + $tagoptions = self::get_tag_options(); + $context = context_system::instance(); + + if (!empty($formdata->tags)) { + \core_tag_tag::set_item_tags( + $tagoptions['component'], $tagoptions['itemtype'], $templateid, $context, $formdata->tags); + } + + // Store actions data. + // Send the data to action plugins for perform the data store. + // Find list of actions. + $actionplugins = new \mod_pulse\plugininfo\pulseaction(); + $plugins = $actionplugins->get_plugins_base(); + + foreach ($plugins as $component => $pluginbase) { + $formdata->templateid = $templateid; + $pluginbase->process_save($formdata, $component); + } + // Allow to update the DB changes to Database. + $transaction->allow_commit(); + + return $templateid; + } + + /** + * Update instance data. + * + * @param int $instanceid + * @param array $options + * @return void + */ + public static function update_instance_data($instanceid, $options) { + global $DB; + + if (isset($options['reference'])) { + $options['reference'] = shorten_text(strip_tags($options['reference']), 30); + } + + if ($record = $DB->get_record('pulse_autotemplates_ins', ['instanceid' => $instanceid])) { + + $diff = array_diff_key((array) $record, $options); + $removeoverrides = array_combine(array_keys($diff), array_fill(0, count($diff), null)); + + $removeoverrides['id'] = $record->id; + $removeoverrides['instanceid'] = $record->instanceid; + $removeoverrides['timemodified'] = date('Y-m-d H:i'); + $removeoverrides = array_merge($removeoverrides, $options); + + return $DB->update_record('pulse_autotemplates_ins', $removeoverrides); + } else { + $options['instanceid'] = $instanceid; + return $DB->insert_record('pulse_autotemplates_ins', $options); + } + + return false; + } + + /** + * Get records for a list of templates. + * + * @param array $templates An array of template IDs. + * @return array An associative array of template records. + */ + public static function get_templates_record($templates) { + global $DB; + + if (empty($templates)) { + return true; + } + // Generate SQL for IN clause and prepare parameters. + list($insql, $inparams) = $DB->get_in_or_equal($templates, SQL_PARAMS_NAMED, 'ins'); + $sql = "SELECT * FROM {pulse_autotemplates} te WHERE te.id $insql"; + // Fetch template records. + $tempoverride = $DB->get_records_sql($sql, $inparams); + + return $tempoverride; + } + +} diff --git a/classes/completion/custom_completion.php b/classes/completion/custom_completion.php index a19833b..8bbb287 100644 --- a/classes/completion/custom_completion.php +++ b/classes/completion/custom_completion.php @@ -54,19 +54,9 @@ public function get_state(string $rule): int { case 'completionwhenavailable': if ($pulse->completionavailable) { $modinfo = get_fast_modinfo($this->cm->course, $this->userid); - $info = new \core_availability\info_module($this->cm); - $str = ''; - // Get section info for cm. - // Check section is accessable by user. - $section = $this->cm->get_section_info(); - $sectioninfo = new \core_availability\info_section($section); - - if ($sectioninfo->is_available($str, false, $this->userid, $modinfo) - && $info->is_available($str, false, $this->userid, $modinfo )) { - $status = COMPLETION_COMPLETE; - } else { - $status = COMPLETION_INCOMPLETE; - } + $cm = $modinfo->get_cm($this->cm->id); + + $status = $cm->get_user_visible() ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } break; case 'completionapproval': @@ -94,7 +84,7 @@ public function get_state(string $rule): int { break; } - return (isset($status) && $status) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; + return (isset($status) && $status) ? $status : COMPLETION_INCOMPLETE; } @@ -144,7 +134,7 @@ public function get_custom_rule_descriptions(): array { $selfstring = get_string('completion:self', 'pulse'); $approvalstring = get_string('completion:approval', 'pulse'); - if (pulse_user_isstudent($this->cm->id)) { + if (\mod_pulse\helper::pulse_user_isstudent($this->cm->id)) { if ( $this->is_available('completionwhenavailable') ) { $state = $this->get_state('completionwhenavailable'); if (in_array($state, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])) { @@ -155,7 +145,7 @@ public function get_custom_rule_descriptions(): array { if ($this->is_available('completionself') ) { $state = $this->get_state('completionself'); if (in_array($state, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])) { - $date = pulse_already_selfcomplete($this->cm->instance, $this->userid); + $date = \mod_pulse\helper::pulse_already_selfcomplete($this->cm->instance, $this->userid); $selfstring = get_string('selfmarked', 'pulse', ['date' => $date]); } } @@ -163,7 +153,7 @@ public function get_custom_rule_descriptions(): array { if ($this->is_available('completionapproval') ) { $state = $this->get_state('completionapproval'); if (in_array($state, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])) { - $message = pulse_user_approved($this->cm->instance, $this->userid); + $message = \mod_pulse\helper::pulse_user_approved($this->cm->instance, $this->userid); $approvalstring = html_to_text($message); } } diff --git a/classes/eventobserver.php b/classes/eventobserver.php index 5d6748b..8b86522 100644 --- a/classes/eventobserver.php +++ b/classes/eventobserver.php @@ -24,8 +24,6 @@ namespace mod_pulse; -defined('MOODLE_INTERNAL') || die('No direct access !'); - /** * Observer class for the course module deleted and user enrolment deleted events. It will remove the user data from pulse. */ @@ -42,6 +40,7 @@ public static function course_module_deleted($event) { global $CFG, $DB; if ($event->other['modulename'] == 'pulse') { $pulseid = $event->other['instanceid']; + $courseid = $event->courseid; // Remove pulse user completion records. if ($DB->record_exists('pulse_completion', ['pulseid' => $pulseid])) { $DB->delete_records('pulse_completion', ['pulseid' => $pulseid]); @@ -68,7 +67,7 @@ public static function user_enrolment_deleted($event) { $userid = $event->relateduserid; // Unenrolled user id. $courseid = $event->courseid; // Retrive list of pulse instance added in course. - $list = pulse_course_instancelist($courseid); + $list = \mod_pulse\helper::course_instancelist($courseid); if (!empty($list)) { $pulselist = array_column($list, 'instance'); list($insql, $inparams) = $DB->get_in_or_equal($pulselist); @@ -78,6 +77,45 @@ public static function user_enrolment_deleted($event) { $DB->delete_records_select('pulse_completion', $select, $inparams); $DB->delete_records_select('pulse_users', $select, $inparams); } + + self::trigger_action_event('user_enrolment_deleted', $event); + return true; } + + /** + * User enrolment trigger actions. + * + * @param [type] $event + * @return void + */ + public static function user_enrolment_created($event) { + $userid = $event->relateduserid; // Unenrolled user id. + $courseid = $event->courseid; + + $list = \mod_pulse\automation\helper::get_course_instances($courseid); + if (!empty($list)) { + foreach ($list as $instanceid => $instance) { + \mod_pulse\automation\instances::create($instanceid)->trigger_action($userid, null, true); + } + } + } + + /** + * Trigger an action event for all instances in a course. + * + * @param string $method The method to trigger. + * @param stdClass $event The event object. + */ + public static function trigger_action_event($method, $event) { + + $courseid = $event->courseid; + + $list = \mod_pulse\automation\helper::get_course_instances($courseid); + if (!empty($list)) { + foreach ($list as $instanceid => $instance) { + \mod_pulse\automation\instances::create($instanceid)->trigger_action_event($method, $event); + } + } + } } diff --git a/classes/extendpro.php b/classes/extendpro.php new file mode 100644 index 0000000..78a91a6 --- /dev/null +++ b/classes/extendpro.php @@ -0,0 +1,283 @@ +. + +/** + * Pulse instance extend features file. contains pro feature extended methods + * + * @package mod_pulse + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse; + +/** + * Extend the pro feature to pulse instances. + */ +class extendpro { + + /** + * Trigger the add pulse instance. + * + * @param mixed $pulseid + * @param mixed $pulse + * @return void + */ + public static function pulse_extend_add_instance($pulseid, $pulse) { + $callbacks = get_plugins_with_function('extend_pulse_add_instance'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($pulseid, $pulse); + } + } + } + + /** + * Trigger pulse extended plugins to do their own update steps. + * + * @param mixed $pulse Pulse instance data. + * @param mixed $context Context module. + * @return void + */ + public static function pulse_extend_update_instance($pulse, $context) { + $callbacks = get_plugins_with_function('extend_pulse_update_instance'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($pulse, $context); + } + } + } + + /** + * Trigger pulse extended plugins delete function to do their own delete steps. + * + * @param mixed $cmid Module context id + * @param mixed $pulseid Pulse instance id. + * @return void + */ + public static function pulse_extend_delete_instance($cmid, $pulseid) { + $callbacks = get_plugins_with_function('extend_pulse_delete_instance'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($cmid, $pulseid); + } + } + } + + /** Inject form elements into mod instance form. + * + * @param mform $mform the form to inject elements into. + * @param mixed $instance Pulse instance. + * @param mixed $method Method of form fields (=reaction only returns the reaction form fields) + * @return void + */ + public static function mod_pulse_extend_form($mform, $instance, $method='') { + $callbacks = get_plugins_with_function('extend_pulse_form'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($mform, $instance, $method); + } + } + } + + /** Extende the pro plugins validation error messages. + * + * @param mixed $data module form submitted data. + * @param mixed $files Module form submitted files. + * @return array list of validation errors. + */ + public static function mod_pulse_extend_formvalidation($data, $files) { + $callbacks = get_plugins_with_function('extend_pulse_validation'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + return $pluginfunction($data, $files); + } + } + } + + /** Inject form elements into mod instance form. + * @param mform $mform the form to inject elements into. + */ + public static function mod_pulse_extend_formdata($mform) { + $callbacks = get_plugins_with_function('extend_pulse_formdata'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($mform); + } + } + } + + /** Extend form post process method from pro plugin. + * @param object $data module form submitted data object. + */ + public static function pulse_extend_postprocessing($data) { + $callbacks = get_plugins_with_function('extend_pulse_postprocessing'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($data); + } + } + } + + /** + * Extended the support of data processing before defalut values are set to form. + * + * @param mixed $defaultvalues Current default values. + * @param mixed $currentinstance status of instance is current (true/false) + * @param mixed $context Module context data record. + * @return void + */ + public static function pulse_extend_preprocessing(&$defaultvalues, $currentinstance, $context) { + $callbacks = get_plugins_with_function('extend_pulse_preprocessing'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $pluginfunction($defaultvalues, $currentinstance, $context); + } + } + } + + /** + * Call the extended email placeholder filters to replace the content. + * + * @param mixed $instance Pulse instance data object. + * @param mixed $displaytype Location to display the reaction. + * @return string $html + */ + public static function pulse_extend_reaction($instance, $displaytype='notification') { + $html = ''; + $callbacks = get_plugins_with_function('extend_pulse_reaction'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $html .= $pluginfunction($instance, $displaytype); + } + } + return $html; + } + + + /** + * Check the pulsepro extended the invitation method. + * if extended the invitation then the invitations are send using pulse pro plugin. + * @return void + */ + public static function pulse_extend_invitation() { + $callbacks = get_plugins_with_function('extend_pulse_invitation'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + return $pluginfunction(); + } + } + } + + /** + * List of extended the function used in the backup steps. + * + * @param mixed $pulse + * @param mixed $userinfo + * @return void + */ + public static function pulse_extend_backup_steps($pulse, $userinfo) { + $callbacks = get_plugins_with_function('extend_pulse_backup_steps'); + if (!empty($callbacks)) { + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + return $pluginfunction($pulse, $userinfo); + } + } + } + return $pulse; + } + + /** + * List of extended plugins restore contents. + * + * @param mixed $contents + * @return void + */ + public static function pulse_extend_restore_content(&$contents) { + $callbacks = get_plugins_with_function('extend_pulse_restore_content'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $contents = $pluginfunction($contents); + } + } + } + + /** + * Extended plugins restore structures used in the acitivty restore. + * + * @param mixed $paths + * @return void + */ + public static function pulse_extend_restore_structure(&$paths) { + $callbacks = get_plugins_with_function('extend_pulse_restore_structure'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $paths = $pluginfunction($paths); + } + } + } + + /** + * List of extended plugins fileareas list to add into pluginfile function. + * + * @return array + */ + public static function pulse_extend_filearea(): array { + $callbacks = get_plugins_with_function('extend_pulse_filearea'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $fileareas = $pluginfunction(); + return $fileareas; + } + } + return []; + } + + /** + * Extend the pro features of preset. Triggered during the import preset data clean. + * + * @param string $method Preset method to extend + * @param array $backupdata Preset template data. + * @return void + */ + public static function pulse_extend_preset($method, &$backupdata) { + $callbacks = get_plugins_with_function('extend_preset_formatdata'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $backupdata = $pluginfunction($method, $backupdata); + } + } + } + + + /** + * Extend the pro features of preset. Convert the record data format into moodle form editor format. + * + * @param string $pulseid Preset method to extend + * @param array $configdata Custom config data. + * @return void + */ + public static function pulse_preset_update($pulseid, $configdata) { + $callbacks = get_plugins_with_function('extend_preset_update'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $backupdata = $pluginfunction($pulseid, $configdata); + } + } + } + +} diff --git a/classes/external.php b/classes/external.php new file mode 100644 index 0000000..36b7117 --- /dev/null +++ b/classes/external.php @@ -0,0 +1,88 @@ +. + +/** + * Load preset fragments. + * + * @package mod_pulse + * @copyright 2021, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/externallib.php'); + +/** + * Pulse preset external definitions. + */ +class external extends \external_api { + + /** + * Create module using selected preset template with the preset configdata. + * + * @return void + */ + public static function apply_presets_parameters() { + + return new \external_function_parameters( + array( + 'contextid' => new \external_value(PARAM_INT, 'The context id for the course'), + 'formdata' => new \external_value(PARAM_RAW, 'The data from the user notes'), + ) + ); + } + + /** + * Service helps to replace the current add module with preset template data. + * + * @param int $contextid Id for module/course context. + * @param string $formdata Custom configurable fields data. + * @param string $pageparams Current module form data. + * @return string + */ + public static function apply_presets(int $contextid, string $formdata, $pageparams = null) { + global $PAGE; + parse_str($formdata, $data); + foreach ($data as $key => $value) { + if (strpos($key, 'preseteditor_') !== false) { + $newkey = str_replace('preseteditor_', '', $key); + $data[$newkey] = $value; + unset($data[$key]); + } + } + $context = \context::instance_by_id($contextid); + $PAGE->set_context($context); + $preset = new \mod_pulse\preset($data['presetid'], $data['courseid'], $context); + if ($pageparams !== null) { + parse_str($pageparams, $params); + $preset->set_modformdata($params); + } + $result = $preset->apply_presets($data); + return $result; + } + + /** + * Retuns the redirect course url and created pulse id for save method. + * + * @return void + */ + public static function apply_presets_returns() { + return new \external_value(PARAM_RAW, 'Count of Page user notes'); + } +} diff --git a/classes/forms/automation_instance_form.php b/classes/forms/automation_instance_form.php new file mode 100644 index 0000000..7d6a257 --- /dev/null +++ b/classes/forms/automation_instance_form.php @@ -0,0 +1,233 @@ +. + +/** + * Automation instance form for the pulse 2.0. + * + * @package mod_pulse + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pulse\forms; + +defined('MOODLE_INTERNAL') || die('No direct access!'); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +use html_writer; +use mod_pulse\automation\templates; + +/** + * Define the automation instance form. + */ +class automation_instance_form extends automation_template_form { + + /** + * After the instance form elements are defined, create its override options for all elemennts, include hidden instance data. + * Remove elements doesn't used in instances. + * + * @return void + */ + public function after_definition() { + global $PAGE; + + $mform =& $this->_form; + + $mform->updateAttributes(['id' => 'pulse-automation-template' ]); + // Courseid. + $course = $this->_customdata['courseid'] ?? ''; + $mform->addElement('hidden', 'courseid', $course); + $mform->setType('courseid', PARAM_INT); + + $templateid = $this->_customdata['templateid'] ?? ''; + $mform->addElement('hidden', 'templateid', $templateid); + $mform->setType('templateid', PARAM_INT); + + $templateid = $this->_customdata['instanceid'] ?? ''; + $mform->addElement('hidden', 'instanceid', $templateid); + $mform->setType('instanceid', PARAM_INT); + + $mform->removeElement('visible'); + $mform->removeElement('categories'); + + // Get the list of elments add in this form. create override button for all elements expect the hidden elements. + $elements = $mform->_elements; + + // Add the Reference element. + $reference = $mform->createElement('text', 'insreference', get_string('reference', 'pulse'), ['size' => '50']); + $mform->insertElementBefore($reference, 'reference'); + $mform->setType('insreference', PARAM_ALPHANUMEXT); + $mform->addRule('insreference', get_string('required'), 'required', null, 'client'); + $mform->addHelpButton('insreference', 'reference', 'pulse'); + + $mform->removeElement('reference'); + + $templatereference = $this->get_customdata('templatereference'); + $input = html_writer::empty_tag('input', + ['class' => 'form-control', + 'type' => 'text', + 'value' => $templatereference, + 'disabled' => 'disabled' + ]); + $referenceprefix = $mform->createElement('html', html_writer::div($input, 'hide', ['id' => 'pulse-template-reference'])); + $mform->insertElementBefore($referenceprefix, 'insreference'); + + $this->load_default_override_elements(['insreference']); + + if (!empty($elements)) { + // List of element type don't need to add the override option. + $dontoverride = ['html', 'header', 'hidden', 'button']; + + foreach ($elements as $element) { + + if (!in_array($element->getType(), $dontoverride) && $element->getName() !== 'buttonar') { + $this->add_override_element($element); + } + } + } + } + + /** + * Add an override element to the form. + * + * @param mixed $element The form element. + */ + protected function add_override_element($element) { + + $mform =& $this->_form; + $elementname = $element->getName(); + $orgelementname = $elementname; + + if (stripos($elementname, "[") !== false) { + $name = str_replace("]", "", str_replace("[", "_", $elementname)); + $name = 'override[' . $name .']'; + } else { + $name = 'override[' . $elementname .']'; + } + + // Override element already exists, no need to create new one. + if (isset($mform->_elementIndex[$name])) { + return; + } + + $overrideelement = $mform->createElement('advcheckbox', $name, '', '', + array('group' => 'automation', 'class' => 'custom-control-input'), array(0, 1)); + + // Insert the override checkbox before the element. + if (isset($mform->_elementIndex[$orgelementname]) && $mform->_elementIndex[$orgelementname]) { + $mform->insertElementBefore($overrideelement, $orgelementname); + } + + // Disable the form fields by default, only enable whens its enabled for overriddden. + $mform->disabledIf($orgelementname, $name, 'notchecked'); + } + + /** + * Includ the pulse conditions element to the instance form. + * + * @return void + */ + protected function load_template_conditions() { + + $mform =& $this->_form; + $mform->addElement('html', '
'); + $mform->addElement('header', 'generalconditions', '

'.get_string('general').'

'); + // Operator element. + $operators = [ + \mod_pulse\automation\action_base::OPERATOR_ALL => get_string('all', 'pulse'), + \mod_pulse\automation\action_base::OPERATOR_ANY => get_string('any', 'pulse'), + ]; + $mform->addElement('select', 'triggeroperator', get_string('triggeroperator', 'pulse'), $operators); + $mform->addHelpButton('triggeroperator', 'triggeroperator', 'pulse'); + + $conditionplugins = new \mod_pulse\plugininfo\pulsecondition(); + $plugins = $conditionplugins->get_plugins_base(); + + foreach ($plugins as $name => $plugin) { + $mform->addElement('header', $name, get_string('pluginname', 'pulsecondition_'.$name)); + + $plugin->load_instance_form($mform, $this); + $plugin->upcoming_element($mform); + $mform->setExpanded($name); + } + $mform->addElement('html', ''); // E.o of actions triggere tab. + $mform->addElement('html', html_writer::end_div()); // E.o of actions triggere tab. + } + + + /** + * Load instance form elments for pulse action plugins. + * + * @return void + */ + protected function load_template_actions() { + $mform =& $this->_form; + $actionplugins = new \mod_pulse\plugininfo\pulseaction(); + $plugins = $actionplugins->get_plugins_base(); + foreach ($plugins as $name => $plugin) { + // Define the form elements inside the definition function. + $mform->addElement('html', '