diff --git a/classes/local/subscription_table.php b/classes/local/subscription_table.php new file mode 100644 index 0000000..a80dbfb --- /dev/null +++ b/classes/local/subscription_table.php @@ -0,0 +1,124 @@ +. + +/** + * View your subscription and change settings + * + * @package local_amos + * @tags MoodleMootDACH2019 + * @copyright 2019 Tobias Reischmann + * @copyright 2019 Martin Gauk + * @copyright 2019 Jan Eberhardt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_amos\local; + + +use local_amos\subscription_manager; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once $CFG->libdir . DIRECTORY_SEPARATOR . 'tablelib.php'; +require_once $CFG->dirroot . '/local/amos/classes/subscription_manager.php'; +require_once $CFG->dirroot . '/local/amos/mlanglib.php'; + +class subscription_table extends \flexible_table { + + public function init($baseurl, $pagesize = -1) + { + global $USER; + + $manager = new subscription_manager($USER->id); + $subs = $manager->fetch_subscriptions(); + + $this->define_baseurl($baseurl); + $this->define_columns([ + 'component', + 'language' + ]); + $this->define_headers([ + get_string('component', 'local_amos'), + get_string('languages', 'local_amos') + ]); + $this->pagesize($pagesize > 0 ? $pagesize : $this->pagesize, count($subs)); + $this->sortable(true, 'component'); + $this->no_sorting('language'); + $this->setup(); + } + + public function out() { + global $USER; + $manager = new subscription_manager($USER->id); + $subs = $manager->fetch_subscriptions(); + $sort = $this->get_sort_columns()['component']; + if ($sort === SORT_ASC) { + ksort($subs); + } else { + krsort($subs); + } + $this->start_output(); + foreach ($subs as $component => $langarray) { + $row = $this->format_row([ + 'component' => $component, + 'language' => $langarray + ]); + $this->add_data_keyed($row); + } + $this->finish_output(); + } + + public function col_component($sub) { + global $PAGE; + $icon = $PAGE->get_renderer('local_amos')->pix_icon( + 't/delete', + get_string('unsubscribe', 'local_amos') + ); + $query_string = sprintf('?c=%s&m=unsubscribe', $sub->component); + $link = \html_writer::link($this->baseurl . $query_string, $icon, ['class' => 'unsubscribe']); + return sprintf('%s %s', $sub->component, $link); + } + + public function col_language($sub) { + global $OUTPUT; + + $inplace = self::get_lang_inplace_editable($sub->component, $sub->language); + return $OUTPUT->render_from_template('core/inplace_editable', $inplace->export_for_template($OUTPUT)); + } + + static public function get_lang_inplace_editable(string $component, array $langs) { + $options = \mlang_tools::list_languages(false); + + $values = json_encode($langs); + $displayvalues = implode(', ', array_map(function($lang) use ($options) { + return (isset($options[$lang])) + ? $options[$lang] + : get_string('unknown_language', 'local_amos', $lang); + }, $langs)); + + $inplace = new \core\output\inplace_editable('local_amos', 'subscription', $component, + true, $displayvalues, $values); + + $attributes = ['multiple' => true]; + $inplace->set_type_autocomplete($options, $attributes); + + return $inplace; + } +} \ No newline at end of file diff --git a/classes/subscription_manager.php b/classes/subscription_manager.php new file mode 100644 index 0000000..f11d13f --- /dev/null +++ b/classes/subscription_manager.php @@ -0,0 +1,189 @@ +. + +/** + * Provides the {@link subscription_manager} class. + * + * @package local_amos + * @copyright 2019 Tobias Reischmann + * @copyright 2019 Martin Gauk + * @copyright 2019 Jan Eberhardt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_amos; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot.'/local/amos/mlanglib.php'); + +/** + * Manager class for accessing and updating the user's subscriptions. + * + * @copyright 2019 Tobias Reischmann + * @copyright 2019 Martin Gauk + * @copyright 2019 Jan Eberhardt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class subscription_manager { + /** @var int user id */ + private $userid; + + /** @var array of array (component -> language) */ + private $subscriptions; + + /** @var array */ + private $addsubscriptions; + + /** @var array */ + private $remsubscriptions; + + /** + * Subscription manager constructor. + * + * @param int $userid user id + */ + public function __construct(int $userid) { + $this->userid = $userid; + $this->subscriptions = []; + $this->addsubscriptions = []; + $this->remsubscriptions = []; + } + + /** + * Fetch all subscriptions of the user. + * + * @return array + */ + public function fetch_subscriptions() { + global $DB; + + $this->subscriptions = []; + $rows = $DB->get_records('amos_subscription', ['userid' => $this->userid]); + foreach ($rows as $row) { + if (!isset($this->subscriptions[$row->component])) { + $this->subscriptions[$row->component] = [$row->lang]; + } else { + $this->subscriptions[$row->component][] = $row->lang; + } + } + + return $this->subscriptions; + } + + /** + * Add new subscription. + * + * @param string $component component name + * @param string $lang language code + */ + public function add_subscription(string $component, string $lang) { + $this->addsubscriptions[] = (object) ['component' => $component, 'lang' => $lang]; + } + + /** + * Remove one subscription. + * + * @param string $component component name + * @param string $lang language code + */ + public function remove_subscription(string $component, string $lang) { + $this->remsubscriptions[] = (object) ['component' => $component, 'lang' => $lang]; + } + + /** + * Remove all language subscriptions of one component. + * + * @param string $component component name + */ + public function remove_component_subscription(string $component) { + $this->remsubscriptions[] = (object) ['component' => $component, 'lang' => null]; + } + + /** + * Apply all changes to the database. + * + * All changes that you registered with add_subscription, remove_subscription and remove_component_subscription. + */ + public function apply_changes() { + global $DB; + + // Get available components and langs. + $components = \mlang_tools::list_components(); + $langs = \mlang_tools::list_languages(); + + $transaction = $DB->start_delegated_transaction(); + + // Remove subscriptions. + foreach ($this->remsubscriptions as $sub) { + if ($sub->lang !== null) { + $DB->delete_records('amos_subscription', [ + 'userid' => $this->userid, + 'component' => $sub->component, + 'lang' => $sub->lang, + ]); + } else { + $DB->delete_records('amos_subscription', [ + 'userid' => $this->userid, + 'component' => $sub->component, + ]); + } + } + + // Refresh subscriptions to check for duplicates. + $this->fetch_subscriptions(); + + $inserts = []; + // Validate component names and language codes of subscriptions that should be added. + foreach ($this->addsubscriptions as $sub) { + if (!isset($components[$sub->component])) { + continue; + } + + if (!isset($langs[$sub->lang])) { + continue; + } + + // Check if not already subscribed. + if (isset($this->subscriptions[$sub->component]) && + in_array($sub->lang, $this->subscriptions[$sub->component])) { + continue; + } + + $sub->userid = $this->userid; + $inserts[] = $sub; + $this->subscriptions[$sub->component][] = $sub->lang; + } + $DB->insert_records('amos_subscription', $inserts); + + $transaction->allow_commit(); + + // Reset list of changes. + $this->addsubscriptions = []; + $this->remsubscriptions = []; + } + + /** + * Remove all subscriptions of the user. + */ + public function remove_all_subscriptions() { + global $DB; + + $DB->delete_records('amos_subscription', ['userid' => $this->userid]); + } +} diff --git a/classes/task/notify_subscribers.php b/classes/task/notify_subscribers.php new file mode 100644 index 0000000..c0ddd00 --- /dev/null +++ b/classes/task/notify_subscribers.php @@ -0,0 +1,83 @@ +. + +/** + * Provides the {@link \local_amos\task\notify_subscribers} class. + * + * @package local_amos + * @category task + * @copyright 2019 Tobias Reischmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_amos\task; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/amos/locallib.php'); + +/** + * Sends notifications about changes of the language packs to the respective subsrcibers. + * + * @copyright 2019 Tobias Reischmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class notify_subscribers extends \core\task\scheduled_task { + + /** + * Return the task name. + * + * @return string + */ + public function get_name() { + return get_string('tasknotifysubscribers', 'local_amos'); + } + + /** + * Execute the task. + */ + public function execute() { + global $DB, $PAGE; + $subject = get_string('subscription_mail_subject', 'local_amos'); + $lasttimerun = get_config('local_amos', 'timesubnotified'); + // For the very first run, we do not want to send out everything that ever happend. + // So we initialize the config with the date from yesterday. + if (!$lasttimerun) { + $today = strtotime('12:00:00'); + $lasttimerun = strtotime('-1 day', $today); + } + $getsql = "SELECT distinct s.userid + FROM {amos_repository} r + JOIN {amos_subscription} s ON (s.lang = r.lang AND s.component = r.component) + WHERE r.timemodified > ?"; + + $users = $DB->get_records_sql($getsql, array($lasttimerun)); + $output = $PAGE->get_renderer('local_amos'); + foreach ($users as $user) { + $user = \core_user::get_user($user->userid); + if ($user) { + $notification_html = new \local_amos_sub_notification($user, $lasttimerun, true); + $notification = new \local_amos_sub_notification($user, $lasttimerun); + + $content_html = $output->render($notification_html); + $content = $output->render($notification); + email_to_user($user, \core_user::get_noreply_user(), $subject, $content, $content_html); + } + } + set_config('timesubnotified', time(), 'local_amos'); + } + +} diff --git a/db/install.xml b/db/install.xml index 9691551..4db058f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -206,5 +206,17 @@ + + + + + + + + + + + +
\ No newline at end of file diff --git a/db/tasks.php b/db/tasks.php index efa2316..f1dd498 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -44,4 +44,13 @@ 'month' => '*', 'dayofweek' => '*', ], + [ + 'classname' => '\local_amos\task\notify_subscribers', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '0', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + ], ]; diff --git a/db/upgrade.php b/db/upgrade.php index 9c50fe4..b3891ef 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -345,5 +345,30 @@ function xmldb_local_amos_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2019040901, 'local', 'amos'); } + if ($oldversion < 2019090900) { + + // Define table amos_subscription to be created. + $table = new xmldb_table('amos_subscription'); + + // Adding fields to table amos_subscription. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('lang', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table amos_subscription. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('fk_user', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']); + + // Conditionally launch create table for amos_subscription. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Amos savepoint reached. + upgrade_plugin_savepoint(true, 2019090900, 'local', 'amos'); + } + + return $result; } diff --git a/lang/en/local_amos.php b/lang/en/local_amos.php index 96f95a0..0ee1be7 100644 --- a/lang/en/local_amos.php +++ b/lang/en/local_amos.php @@ -27,6 +27,7 @@ $string['about'] = '

AMOS is a central repository of Moodle strings and their history. It tracks the addition of English strings into Moodle code, gathers translations, handles common translation tasks and generates language packages to be deployed on Moodle servers.

See AMOS documentation for more information.

'; +$string['addsubscription'] = 'Add Subscription'; $string['amos'] = 'AMOS translation toolkit'; $string['amos:changecontriblang'] = 'Change language of contributed strings'; $string['amos:commit'] = 'Commit the staged strings into the main repository'; @@ -47,6 +48,7 @@ $string['commitstage_help'] = 'Permanently store all staged translations in AMOS repository. Stage is automatically pruned and rebased before it is committed. Only committable strings are stored. That means that only translations below highlighted in green will be stored. The stage is cleared after the commit.'; $string['committableall'] = 'all languages'; $string['committablenone'] = 'no languages allowed - please contact AMOS manager'; +$string['component'] = 'Component'; $string['componentsall'] = 'All'; $string['componentsapp'] = 'Moodle App'; $string['componentsenlarge'] = 'Enlarge'; @@ -197,6 +199,8 @@ $string['diffstrings'] = 'Compare strings at two branches'; $string['diffstrings_help'] = 'This will compare all strings at the two selected branches. If there is difference in strings on the branches, both versions are staged. You can then use "Edit staged strings" feature to review and fix the changes as needed.'; $string['diffversions'] = 'Versions'; +$string['emailintroduction'] = 'Dear {$a->firstname} {$a->lastname},
+ the following string changes were made in AMOS for your subscripted plugins:'; $string['err_exception'] = 'Error: {$a}'; $string['err_invalidlangcode'] = 'Invalid language code'; $string['err_parser'] = 'Parsing error: {$a}'; @@ -289,6 +293,7 @@ $string['messageprovider:contribution'] = 'Contributed translations'; $string['morefilteringoptions'] = 'More options'; $string['newlanguage'] = 'New language'; +$string['newstring'] = 'New string'; $string['nodiffs'] = 'No differences found'; $string['nofiletoimport'] = 'Please provide a file to import from.'; $string['nologsfound'] = 'No strings found, please modify filters'; @@ -301,6 +306,7 @@ $string['numofcommitsabovelimit'] = 'Found {$a->found} commits matching the commit filter, using {$a->limit} most recent'; $string['numofcommitsunderlimit'] = 'Found {$a->found} commits matching the commit filter'; $string['numofmatchingstrings'] = 'Within that, {$a->strings} modifications in {$a->commits} commits match the string filter'; +$string['oldstring'] = 'Old string'; $string['outdatednotcommitted'] = 'Outdated string'; $string['outdatednotcommitted_help'] = 'AMOS detected that the string may be outdated as the English version was modified after it had been translated. Please review the translation.'; $string['outdatednotcommittedwarning'] = 'Outdated'; @@ -448,10 +454,15 @@ $string['stashtitle'] = 'Stash title'; $string['stashtitledefault'] = 'Work in progress - {$a->time}'; $string['stringhistory'] = 'History'; +$string['stringidentifier'] = 'String identifier'; $string['strings'] = 'Strings'; $string['submitting'] = 'Submitting a contribution'; $string['submitting_help'] = 'This will send translated strings to official language maintainers. They will be able to apply your work into their stage, review it and eventually commit. Please provide a message for them describing your work and why you would like to see your contribution included.'; +$string['subscribe_info'] = 'You have been subscribed successfully'; +$string['subscription'] = 'Subscription'; +$string['subscription_mail_subject'] = 'AMOS Notification - New language pack changes'; $string['targetversion'] = 'Target version'; +$string['tasknotifysubscribers'] = 'Send Notifications to Subscribers'; $string['timeline'] = 'timeline'; $string['translatortool'] = 'Translator'; $string['translatortoolopen'] = 'Open AMOS translator'; @@ -471,9 +482,12 @@ $string['unableenfixaddon'] = 'English fixes allowed for standard plugins only'; $string['unableenfixcountries'] = 'Country names are copied from ISO 3166-1'; $string['unableunmaintained'] = 'The language pack \'{$a}\' has no maintainer at the moment, so translation contributions cannot be accepted. Please consider volunteering to become \'{$a}\' language pack maintainer.'; +$string['unknown_language'] = 'Unknown language ({$a})'; $string['unstage'] = 'Unstage'; $string['unstageconfirm'] = 'Really?'; $string['unstaging'] = 'Unstaging'; +$string['unsubscribe'] = 'Unsubscribe'; +$string['unsubscribe_info'] = 'You have been unsubscribed successfully'; $string['untranslate'] = 'untranslate'; $string['untranslateconfirm'] = '

You are going to remove existing translation of the string {$a->stringid}, component {$a->component}, from all the versions of the language pack {$a->language}.

Are you sure?

'; $string['untranslatetitle'] = 'Removing translation from the language pack'; diff --git a/lib.php b/lib.php index be26d3e..a533ac8 100644 --- a/lib.php +++ b/lib.php @@ -44,6 +44,7 @@ function local_amos_extend_navigation(global_navigation $navigation) { $amos->add(get_string('contributions', 'local_amos'), new moodle_url('/local/amos/contrib.php'), navigation_node::TYPE_CUSTOM, null, 'contributions'); } $amos->add(get_string('log', 'local_amos'), new moodle_url('/local/amos/log.php'), navigation_node::TYPE_CUSTOM, null, 'log'); + $amos->add(get_string('subscription', 'local_amos'), new moodle_url('/local/amos/subscription.php'), navigation_node::TYPE_CUSTOM, 'subscription'); $amos->add(get_string('creditstitleshort', 'local_amos'), new moodle_url('/local/amos/credits.php'), navigation_node::TYPE_CUSTOM, null, 'credits'); if (has_capability('local/amos:manage', context_system::instance())) { $admin = $amos->add(get_string('administration')); @@ -184,3 +185,45 @@ function local_amos_comment_template($params) { return $template; } + +/** + * Callback to update a value in an inplace_editable. + * + * @param $itemtype + * @param $itemid + * @param $newvalue + */ +function local_amos_inplace_editable($itemtype, $itemid, $newvalue) { + global $USER; + + \external_api::validate_context(context_system::instance()); + + if ($itemtype === 'subscription') { + $component = clean_param($itemid, PARAM_ALPHANUMEXT); + $langs = json_decode($newvalue); + + $manager = new \local_amos\subscription_manager($USER->id); + $manager->remove_component_subscription($component); + + foreach ($langs as $lang) { + $lang = clean_param($lang, PARAM_ALPHANUMEXT); + $manager->add_subscription($component, $lang); + } + + $manager->apply_changes(); + $subs = $manager->fetch_subscriptions(); + $newlangs = (isset($subs[$component])) ? $subs[$component] : []; + + return \local_amos\local\subscription_table::get_lang_inplace_editable($component, $newlangs); + } +} + +/** + * Callback to remove user subscriptions when user will be deleted. + * + * @param $user + */ +function local_amos_pre_user_delete($user) { + $manager = new \local_amos\subscription_manager($user->id); + $manager->remove_all_subscriptions(); +} diff --git a/locallib.php b/locallib.php index d3e718e..e328ba0 100644 --- a/locallib.php +++ b/locallib.php @@ -1527,6 +1527,137 @@ public function __construct(stdClass $info, stdClass $author=null, stdClass $ass } } +/** + * Represents a collection changes to be sent to subscribers + */ +class local_amos_sub_notification implements renderable, templatable { + + /** @var array of $components including the changes made for string od them*/ + public $components = array(); + + /** @var stdClass user */ + public $user; + + /** @var bool $html States if the notification should be */ + public $html; + + /** + * Fetches the required commits from the repository + * + * @param stdClass $user id of the subscriber + * @param int $since timestamp since when changes should be returned. + * @param bool $html States if the notification should be + */ + public function __construct($user, $since, $html = false) { + global $DB; + + // Get all unique combinations of stringid, component, lang and branch. + $getsqlrelevantstrings = "SELECT r.stringid, + r.component, + r.lang, + r.branch, + max(r.timemodified) timemodified + FROM {amos_commits} co + JOIN {amos_repository} r ON (co.id = r.commitid) + JOIN {amos_texts} t ON (r.textid = t.id) + JOIN {amos_subscription} s ON (s.lang = r.lang AND s.component = r.component) + WHERE r.timemodified > ? + GROUP BY r.stringid, r.component, r.lang, r.branch"; + + // Query the newest and oldest repository ids for the respective relevant string combinations. + $getsqlforoldmodified = "SELECT relevantstrings.stringid, + relevantstrings.component, + relevantstrings.lang, + relevantstrings.branch, + relevantstrings.timemodified newmodified, + max(oldtexts.timemodified) oldmodified + FROM ($getsqlrelevantstrings) as relevantstrings LEFT JOIN + {amos_repository} oldtexts ON + relevantstrings.stringid = oldtexts.stringid AND + relevantstrings.component = oldtexts.component AND + relevantstrings.lang = oldtexts.lang AND + relevantstrings.branch = oldtexts.branch AND + oldtexts.timemodified < ? + GROUP BY relevantstrings.stringid, + relevantstrings.component, + relevantstrings.lang, + relevantstrings.branch, + relevantstrings.timemodified"; + + $getrepoids = "SELECT relevantstrings.stringid, + relevantstrings.component, + relevantstrings.lang, + relevantstrings.branch, + max(newrepo.textid) newtextid, + max(oldrepo.textid) oldtextid + FROM ($getsqlforoldmodified) as relevantstrings JOIN + {amos_repository} newrepo ON + relevantstrings.stringid = newrepo.stringid AND + relevantstrings.component = newrepo.component AND + relevantstrings.lang = newrepo.lang AND + relevantstrings.branch = newrepo.branch AND + relevantstrings.newmodified = newrepo.timemodified LEFT JOIN + {amos_repository} oldrepo ON + relevantstrings.stringid = oldrepo.stringid AND + relevantstrings.component = oldrepo.component AND + relevantstrings.lang = oldrepo.lang AND + relevantstrings.branch = oldrepo.branch AND + relevantstrings.oldmodified = oldrepo.timemodified + GROUP BY relevantstrings.stringid, + relevantstrings.component, + relevantstrings.lang, + relevantstrings.branch"; + + // Actually query the result containing the identifiers of the unique combinaiton. + // And also the old and the new texts. + $getsql = "SELECT innersql.stringid, + innersql.component, + innersql.lang, + innersql.branch, + newtext.text newtext, + oldtext.text oldtext FROM + ($getrepoids) as innersql JOIN + {amos_texts} newtext ON + innersql.newtextid = newtext.id LEFT JOIN + {amos_texts} oldtext ON + innersql.oldtextid = oldtext.id"; + $recordset = $DB->get_recordset_sql($getsql, array($since, $since)); + + // Build the data format for the mustach template. + foreach ($recordset as $record) { + $identifier = $record->component . '#' . $record->lang; + if (!array_key_exists($identifier, $this->components)) { + $component = new stdClass(); + $component->name = $record->component; + $component->lang = $record->lang; + $component->changes = []; + $this->components[$identifier] = $component; + } + $component = $this->components[$identifier]; + $change = new stdClass(); + $change->newtext = $record->newtext; + $change->oldtext = $record->oldtext; + $change->stringid = $record->stringid; + $change->branch = mlang_version::by_code($record->branch)->label; + $component->changes [] = $change; + } + + // Remove the text keys and replace them by random ints in order for mustach to work. + $this->components = array_values($this->components); + + $this->user = $user; + + $this->html = $html; + } + + /** + * The class already stores the data in the correct structure, so it returns itself. + */ + public function export_for_template(renderer_base $output) { + return $this; + } +} + /** * Returns the list of standard components * @@ -1767,3 +1898,13 @@ function local_amos_simplediff(array $old, array $new) { array_slice($new, $nmax, $maxlen), local_amos_simplediff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen))); } + +class local_amos_subscription_filter extends local_amos_filter { + + public function __construct(moodle_url $handler) + { + parent::__construct($handler); + $this->fields = ['component', 'language']; + } + +} diff --git a/renderer.php b/renderer.php index a14ad75..190b762 100644 --- a/renderer.php +++ b/renderer.php @@ -30,21 +30,47 @@ */ class local_amos_renderer extends plugin_renderer_base { + protected function render_local_amos_subscription_filter(local_amos_subscription_filter $filter) { + // initialize + $output = ''; + $formattributes = [ + 'action' => $filter->handler, + 'method' => 'post', + 'id' => 'amosfilter_form' + ]; + + // start form + $output .= html_writer::start_div('filterwrapper'); + $output .= html_writer::start_tag('form', $formattributes); + $output .= html_writer::start_tag('fieldset', ['id' => 'amosfilter']); + + $alerts = []; + $defaults = (object) ['language' => [], 'component' => [], 'last' => true]; + $output .= $this->render_local_amos_filter_basic($defaults, $alerts, false, false); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'value' => 'subscribe', 'name' => 'm']); + + // close form + $output .= html_writer::end_tag('fieldset'); + $button = ''; + $output .= html_writer::div($button, 'form-actions'); + $output .= html_writer::end_tag('form'); + $output .= html_writer::end_div(); + + return $output; + } + /** - * Renders the filter form + * Renders basic filter of the filter form * - * @param local_amos_filter $filter + * @param object $filterdata * @return string */ - protected function render_local_amos_filter(local_amos_filter $filter) { + protected function render_local_amos_filter_basic($filterdata, &$alerts, $showappfilterbutton = true, $showselectionerror = true) { $output = ''; - $alerts = array(); - - $filterdata = $filter->get_data(); // language selector $current = $filterdata->language; - $someselected = false; + $someselected = !$showselectionerror; $options = mlang_tools::list_languages(false); foreach ($options as $langcode => $langname) { if (!$someselected and in_array($langcode, $current)) { @@ -68,7 +94,7 @@ protected function render_local_amos_filter(local_amos_filter $filter) { $output .= html_writer::start_tag('div', array('class' => 'controls')); $output .= html_writer::select($options, 'flng[]', $current, '', - array('id' => 'amosfilter_flng', 'multiple' => 'multiple', 'size' => 3)); + array('id' => 'amosfilter_flng', 'multiple' => 'multiple', 'size' => 3)); $radio = html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'amosfilter_flng_actions', 'autocomplete' => 'off')); @@ -129,7 +155,7 @@ protected function render_local_amos_filter(local_amos_filter $filter) { asort($optionscontrib); $current = $filterdata->component; - $someselected = false; + $someselected = !$showselectionerror; $app_plugins = local_amos_app_plugins(); @@ -203,8 +229,10 @@ protected function render_local_amos_filter(local_amos_filter $filter) { $radio = html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'amosfilter_fcmp_actions', 'autocomplete' => 'off')); $fcmpactions = html_writer::tag('label', $radio.' '.get_string('componentsstandard', 'local_amos'), array('id' => 'amosfilter_fcmp_actions_allstandard', 'class' => 'btn btn-light amosfilter_fcmp_actions_select')); - $radio = html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'amosfilter_fcmp_actions', 'autocomplete' => 'off')); - $fcmpactions .= html_writer::tag('label', $radio.' '.get_string('componentsapp', 'local_amos'), array('id' => 'amosfilter_fcmp_actions_allapp', 'class' => 'btn btn-light amosfilter_fcmp_actions_select')); + if ($showappfilterbutton) { + $radio = html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'amosfilter_fcmp_actions', 'autocomplete' => 'off')); + $fcmpactions .= html_writer::tag('label', $radio . ' ' . get_string('componentsapp', 'local_amos'), array('id' => 'amosfilter_fcmp_actions_allapp', 'class' => 'btn btn-light amosfilter_fcmp_actions_select')); + } $radio = html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'amosfilter_fcmp_actions', 'autocomplete' => 'off')); $fcmpactions .= html_writer::tag('label', $radio.' '.get_string('componentsall', 'local_amos'), array('id' => 'amosfilter_fcmp_actions_all', 'class' => 'btn btn-light amosfilter_fcmp_actions_select')); @@ -226,6 +254,24 @@ protected function render_local_amos_filter(local_amos_filter $filter) { $output .= html_writer::end_tag('div'); // .controls $output .= html_writer::end_tag('div'); // .control-group + return $output; + } + + /** + * Renders the filter form + * + * @param local_amos_filter $filter + * @return string + */ + protected function render_local_amos_filter(local_amos_filter $filter) { + $output = ''; + $alerts = array(); + + $filterdata = $filter->get_data(); + + // render language and component filter + $output .= $this->render_local_amos_filter_basic($filterdata, $alerts); + // version checkboxes $current = $filterdata->version; $someselected = false; @@ -1587,6 +1633,23 @@ protected function render_local_amos_contribution(local_amos_contribution $contr return $output; } + /** + * Render repository records + * + * @param param local_amos_sub_notification $records of stdclass amos changes + * @return string HTML + */ + protected function render_local_amos_sub_notification(local_amos_sub_notification $notification) { + global $OUTPUT; + if ($notification->html) { + $template = 'local_amos/subscription_notification_html'; + } else { + $template = 'local_amos/subscription_notification'; + } + return $OUTPUT->render_from_template($template, $notification->export_for_template($OUTPUT)); + + } + /** * Renders the AMOS credits page * diff --git a/subscription.php b/subscription.php new file mode 100644 index 0000000..39385f9 --- /dev/null +++ b/subscription.php @@ -0,0 +1,119 @@ +. + +/** + * View your subscription and change settings + * + * @package local-amos + * @copyright 2019 Tobias Reischmann + * @copyright 2019 Martin Gauk + * @copyright 2019 Jan Eberhardt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use local_amos\subscription_manager; + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once(dirname(__FILE__).'/mlanglib.php'); + +require_login(SITEID, false); + +#$name = optional_param('name', null, PARAM_RAW); // stash name +$mode = optional_param('m', null, PARAM_ALPHA); +$PAGE->set_pagelayout('standard'); +$PAGE->set_url('/local/amos/subscription.php'); +$PAGE->set_title('AMOS ' . get_string('subscription', 'local_amos')); +$PAGE->set_heading('AMOS ' . get_string('subscription', 'local_amos')); +$PAGE->requires->strings_for_js(array('processing', 'googletranslate'), 'local_amos'); +$PAGE->requires->yui_module('moodle-local_amos-filter', 'M.local_amos.init_filter', null, null, true); +$PAGE->requires->yui_module('moodle-local_amos-timeline', 'M.local_amos.init_timeline', null, null, true); +$output = $PAGE->get_renderer('local_amos'); +echo $output->header(); + +# add subscription +if ($mode === 'subscribe') { + $languages = optional_param_array('flng', null, PARAM_ALPHA); + $components = optional_param_array('fcmp', null, PARAM_ALPHAEXT); + $error = false; + if (empty($components)) { + echo html_writer::div( + get_string('filtercmpnothingselected', 'local_amos'), + 'alert alert-danger', + ['role' => 'alert'] + ); + $error = true; + } + if (empty($languages)) { + echo html_writer::div( + get_string('filterlngnothingselected', 'local_amos'), + 'alert alert-danger', + ['role' => 'alert'] + ); + $error = true; + } + if (!$error) { + $subscribed = false; + $manager = new subscription_manager($USER->id); + $clist = mlang_tools::list_components(); + $llist = mlang_tools::list_languages(); + foreach ($components as $cmp) { + if (isset($clist[$cmp])) { + foreach ($languages as $lang) { + if (isset($llist[$lang])) { + $manager->add_subscription($cmp, $lang); + $subscribed = true; + } + } + } + } + $manager->apply_changes(); + if ($subscribed) { + echo html_writer::div( + get_string('subscribe_info', 'local_amos'), 'alert alert-success', ['role' => 'alert']); + } + } +} + +if ($mode === 'unsubscribe') { + $language = optional_param('l', null, PARAM_ALPHAEXT); + $component = optional_param('c', null, PARAM_ALPHAEXT); + if (!empty($component)) { + $manager = new subscription_manager($USER->id); + if (empty($language)) { + $manager->remove_component_subscription($component); + + } else { + $manager->remove_subscription($component, $language); + } + $manager->apply_changes(); + echo html_writer::div( + get_string('unsubscribe_info', 'local_amos'), + 'alert alert-success', + ['role' => 'alert'] + ); + } +} + +$filter = new local_amos_subscription_filter($PAGE->url); +$table = new local_amos\local\subscription_table('subscription_table'); + +$table->init($PAGE->url, 40); + +echo $output->render($filter); +$table->out(); +echo $output->footer(); \ No newline at end of file diff --git a/templates/subscription_notification.mustache b/templates/subscription_notification.mustache new file mode 100644 index 0000000..fcf8ae7 --- /dev/null +++ b/templates/subscription_notification.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_amos/subscription_notification + + Template for the notification of langpack changes to a subscriber. + + Context variables required for this template: + * user + * changes - list of changes + + Example context (json): + { "user": { + "firstname": "Mr", + "lastname": "T", + }, + "components": [ + { + "name": "admin", + "lang": "de", + changes: [ + branch: "3.7" + oldtext: "Admin" + newtext: "Admin Plugin" + ], + } + ] + } +}} + + +Dear {{user.firstname}} {{user.lastname}}, + +the following string changes were made in AMOS for your subscripted plugins: + +{{#components}} + {{{name}}} ({{{lang}}}): + + {{#changes}} + String identifier: {{{ stringid }}} + Version: {{{ branch }}} + Old string: {{{ oldtext }}} + New string: {{{ newtext }}} + {{/changes}} +{{/components}} + + diff --git a/templates/subscription_notification_html.mustache b/templates/subscription_notification_html.mustache new file mode 100644 index 0000000..5b5b396 --- /dev/null +++ b/templates/subscription_notification_html.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_amos/subscription_notification + + Template for the notification of langpack changes to a subscriber. + + Context variables required for this template: + * user + * changes - list of changes + + Example context (json): + { "user": { + "firstname": "Mr", + "lastname": "T", + }, + "components": [ + { + "name": "admin", + "lang": "de", + changes: [ + branch: "3.7" + oldtext: "Admin" + newtext: "Admin Plugin" + ], + } + ] + } +}} + + +

{{# str }} emailintroduction, local_amos, { "firstname": "{{user.firstname}}", "lastname": "{{user.lastname}}" }{{/ str }}

+ +
+ {{#components}} +

{{{name}}} ({{{lang}}}):

+ + + + + + + + {{#changes}} + + + + + + + {{/changes}} +
{{# str }} stringidentifier, local_amos {{/ str }}{{# str }} version, local_amos {{/ str }}{{# str }} oldstring, local_amos {{/ str }}{{# str }} newstring, local_amos {{/ str }}
+ {{{ stringid }}} + + {{{ branch }}} + + {{{ oldtext }}} + + {{{ newtext }}} +
+ {{/components}} +
diff --git a/tests/notify_subscribers_test.php b/tests/notify_subscribers_test.php new file mode 100644 index 0000000..58ac0b7 --- /dev/null +++ b/tests/notify_subscribers_test.php @@ -0,0 +1,333 @@ +. + +/** + * Provides the {@link local_amos_notify_subscribers_testcase} class. + * + * @package local_amos + * @category test + * @copyright 2019 Tobias Reischmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/local/amos/mlanglib.php'); + +/** + * Unit tests for the AMOS notify_subscribers_tasks. + * + * @copyright 2019 Tobias Reischmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_amos_notify_subscribers_testcase extends advanced_testcase { + + /** + * Test notifications only print most recent changes. + */ + public function test_notifications_only_most_recent() { + global $DB; + $this->resetAfterTest(true); + + $generator = self::getDataGenerator(); + + $user1 = $generator->create_user(array('mailformat' => 2)); + + $today = strtotime('12:00:00'); + $alsotoday = strtotime('11:00:00'); + $past = strtotime('-12 day', $today); + + $component = new mlang_component('admin', 'de', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'IntermediateString',$alsotoday)); + $stage->add($component); + $stage->commit('Alter pluginname', array('source' => 'amos')); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'de'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + $this->assertCount(1, $sink->get_messages()); + $this->assertContains("OldString", $sink->get_messages()[0]->body); + $this->assertContains("NewString", $sink->get_messages()[0]->body); + $this->assertNotContains("IntermediateString", $sink->get_messages()[0]->body); + $sink->close(); + } + + /** + * Test that multiple changes can be displayed in the notification. + */ + public function test_multiple_subscriptions_one_user() { + global $DB; + $this->resetAfterTest(true); + + $generator = self::getDataGenerator(); + + // Print plain format, since htmlformat creates linebreaks within strings. + $user1 = $generator->create_user(array('mailformat' => 2)); + + $today = strtotime('12:00:00'); + $past = strtotime('-12 day', $today); + + $component = new mlang_component('admin', 'de', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + $component = new mlang_component('theme_boost', 'cs', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldThemeString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewThemeString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'de'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'cs'; + $record->component = 'theme_boost'; + $DB->insert_record('amos_subscription', $record); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + $this->assertCount(1, $sink->get_messages()); + $this->assertContains("OldThemeString", $sink->get_messages()[0]->body); + $this->assertContains("NewThemeString", $sink->get_messages()[0]->body); + $sink->close(); + } + + /** + * Test that multiple subscriptions to multiple users work. + */ + public function test_multiple_subscriptions_multiple_users() { + global $DB; + $this->resetAfterTest(true); + + $generator = self::getDataGenerator(); + + // Print plain format, since htmlformat creates linebreaks within strings. + $user1 = $generator->create_user(array('mailformat' => 2)); + $user2 = $generator->create_user(array('mailformat' => 2)); + + $today = strtotime('12:00:00'); + $past = strtotime('-12 day', $today); + + $component = new mlang_component('admin', 'de', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + $component = new mlang_component('theme_boost', 'cs', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldThemeString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewThemeString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'de'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user2->id; + $record->lang = 'cs'; + $record->component = 'theme_boost'; + $DB->insert_record('amos_subscription', $record); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + $this->assertCount(2, $sink->get_messages()); + $sink->close(); + } + + /** + * Test that old and unsubscribed changes do not cause notification. + */ + public function test_no_matching_subscription() { + global $DB; + $this->resetAfterTest(true); + + $generator = self::getDataGenerator(); + + // Print plain format, since htmlformat creates linebreaks within strings. + $user1 = $generator->create_user(array('mailformat' => 2)); + $user2 = $generator->create_user(array('mailformat' => 2)); + + $today = strtotime('12:00:00'); + $past = strtotime('-12 day', $today); + $past2 = strtotime('-12 day', $today); + + // This change is old. + $component = new mlang_component('admin', 'de', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewString', $past2)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // This change is new but not subscribed. + $component = new mlang_component('theme_boost', 'cs', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldThemeString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewThemeString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'cs'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'de'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user2->id; + $record->lang = 'cs'; + $record->component = 'theme_boost_campus'; + $DB->insert_record('amos_subscription', $record); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + // There should be no + $this->assertCount(0, $sink->get_messages()); + $sink->close(); + } + + /** + * Test that old and unsubscribed changes do not cause notification. + */ + public function test_only_subscriptions_since_lastrun() { + global $DB; + $this->resetAfterTest(true); + + $generator = self::getDataGenerator(); + + // Print plain format, since htmlformat creates linebreaks within strings. + $user1 = $generator->create_user(array('mailformat' => 2)); + + $today = strtotime('12:00:00'); + $yesterday = strtotime('-1 day', $today); + $past = strtotime('-12 day', $today); + + // This change is old. + $component = new mlang_component('admin', 'de', mlang_version::by_branch('MOODLE_36_STABLE')); + $component->add_string(new mlang_string('pluginname', 'OldPluginString', $past)); + $stage = new mlang_stage(); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'bot'), true); + $component->unlink_string('pluginname'); + $component->add_string(new mlang_string('pluginname', 'NewPluginString', $yesterday)); + $component->add_string(new mlang_string('mystring', 'NewString', $today)); + $stage->add($component); + $stage->commit('Add pluginname', array('source' => 'amos')); + $component->clear(); + + // Add a subscription. + $record = new stdClass(); + $record->userid = $user1->id; + $record->lang = 'de'; + $record->component = 'admin'; + $DB->insert_record('amos_subscription', $record); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + // There should be no + $this->assertCount(1, $sink->get_messages()); + $this->assertContains("NewString", $sink->get_messages()[0]->body); + $this->assertNotContains("NewPluginString", $sink->get_messages()[0]->body); + $sink->close(); + + $daybeforeyesterday = strtotime('-2 day', $today); + set_config('timesubnotified', $daybeforeyesterday, 'local_amos'); + + $sink = $this->redirectEmails(); + $task = new \local_amos\task\notify_subscribers(); + $task->execute(); + // There should be no + $this->assertCount(1, $sink->get_messages()); + $this->assertContains("NewString", $sink->get_messages()[0]->body); + $this->assertContains("NewPluginString", $sink->get_messages()[0]->body); + $sink->close(); + } +} diff --git a/tests/subscription_manager_test.php b/tests/subscription_manager_test.php new file mode 100644 index 0000000..8def37e --- /dev/null +++ b/tests/subscription_manager_test.php @@ -0,0 +1,127 @@ +. + +/** + * Provides the {@link local_amos_subscription_manager_test} class. + * + * @package local_amos + * @category test + * @copyright 2019 Tobias Reischmann + * @copyright 2019 Martin Gauk + * @copyright 2019 Jan Eberhardt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot.'/local/amos/mlanglib.php'); + +/** + * Tests for the {@link local_amos\subscription_manager} class. + */ +class local_amos_subscription_manager_test extends advanced_testcase { + + /** + * Helper method to quickly register a language on the given branch(-es) + * + * @param string $langcode the code of the language, such as 'en' + * @param int|array $branchcodes the code of the branch or a list of them + */ + protected function register_language($langcode, $branchcodes) { + + if (!is_array($branchcodes)) { + $branchcodes = array($branchcodes); + } + + $stage = new mlang_stage(); + + foreach ($branchcodes as $branchcode) { + $component = new mlang_component('langconfig', $langcode, mlang_version::by_code($branchcode)); + $component->add_string(new mlang_string('thislanguage', $langcode)); + $component->add_string(new mlang_string('thislanguageint', $langcode)); + $stage->add($component); + $component->clear(); + } + + $stage->commit('Register language '.$langcode, array('source' => 'unittest')); + } + + /** + * Test the workflow of fetching, adding and removing subscriptions. + */ + public function test_subscriptions() { + $this->resetAfterTest(true); + + $this->register_language('en', mlang_version::MOODLE_36); + $this->register_language('de', mlang_version::MOODLE_36); + $this->register_language('fr', mlang_version::MOODLE_36); + $this->register_language('cz', mlang_version::MOODLE_36); + + // Test subscriptions of one user. + $manager = new \local_amos\subscription_manager(2); + $manager->add_subscription('langconfig', 'de'); + $manager->add_subscription('langconfig', 'fr'); + $manager->add_subscription('langconfig', 'cz'); + $manager->add_subscription('langconfig', 'aa'); // Should not be added as it is invalid. + $manager->apply_changes(); + + $subs = $manager->fetch_subscriptions(); + $this->assertArrayHasKey('langconfig', $subs); + $this->assertTrue(in_array('de', $subs['langconfig'])); + $this->assertTrue(in_array('fr', $subs['langconfig'])); + $this->assertTrue(in_array('cz', $subs['langconfig'])); + $this->assertFalse(in_array('aa', $subs['langconfig'])); + $this->assertEquals(3, count($subs['langconfig'])); + + // Try to add one language again. + $manager->add_subscription('langconfig', 'de'); + $manager->apply_changes(); + $subs = $manager->fetch_subscriptions(); + $this->assertEquals(3, count($subs['langconfig'])); + + // Remove one language. + $manager->remove_subscription('langconfig', 'de'); + $manager->apply_changes(); + + $subs = $manager->fetch_subscriptions(); + $this->assertArrayHasKey('langconfig', $subs); + $this->assertFalse(in_array('de', $subs['langconfig'])); + $this->assertTrue(in_array('fr', $subs['langconfig'])); + $this->assertTrue(in_array('cz', $subs['langconfig'])); + + // Remove all other languages for one component. + $manager->remove_component_subscription('langconfig'); + $manager->apply_changes(); + + $subs = $manager->fetch_subscriptions(); + $this->assertEmpty($subs); + + // Add languages again and remove all. + $manager->add_subscription('langconfig', 'de'); + $manager->add_subscription('langconfig', 'fr'); + $manager->add_subscription('langconfig', 'cz'); + $manager->apply_changes(); + + $subs = $manager->fetch_subscriptions(); + $this->assertEquals(3, count($subs['langconfig'])); + + $manager->remove_all_subscriptions(); + $subs = $manager->fetch_subscriptions(); + $this->assertEmpty($subs); + } +} diff --git a/version.php b/version.php index 0c037b6..a56e39f 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'local_amos'; -$plugin->version = 2019040901; +$plugin->version = 2019090901; $plugin->release = '3.6.3'; $plugin->maturity = MATURITY_STABLE; $plugin->requires = 2018120300; diff --git a/yui/filter/filter.js b/yui/filter/filter.js index e9d9eda..8756fe4 100644 --- a/yui/filter/filter.js +++ b/yui/filter/filter.js @@ -73,19 +73,21 @@ YUI.add('moodle-local_amos-filter', function(Y) { fcmp.all('tr.standard:not(.hidden) input').set('checked', true); }); var fcmpselectallapp = filter.one('#amosfilter_fcmp_actions_allapp'); - fcmpselectallapp.on('click', function(e) { - // Check all displayed standard components. - e.preventDefault(); - filter.all('.amosfilter_fcmp_actions_select').removeClass('active'); - fcmp.all('tr:not(.hidden) input').set('checked', false); - fcmp.all('tr.app:not(.hidden) input').set('checked', true); - filter.one('#fapp input').set('checked', true); - filter.one('#amosfilter_fmis_collapse').removeClass('collapse'); - filter.one('#fapp').ancestor().removeClass('collapse'); - filter.all('#amosfilter_fver .fver').set('disabled', true); - filter.one('#amosfilter_fver_versions').addClass('hidden'); - filter.one('#amosfilter_fver #flast').set('checked', true); - }); + if (fcmpselectallapp) { + fcmpselectallapp.on('click', function (e) { + // Check all displayed standard components. + e.preventDefault(); + filter.all('.amosfilter_fcmp_actions_select').removeClass('active'); + fcmp.all('tr:not(.hidden) input').set('checked', false); + fcmp.all('tr.app:not(.hidden) input').set('checked', true); + filter.one('#fapp input').set('checked', true); + filter.one('#amosfilter_fmis_collapse').removeClass('collapse'); + filter.one('#fapp').ancestor().removeClass('collapse'); + filter.all('#amosfilter_fver .fver').set('disabled', true); + filter.one('#amosfilter_fver_versions').addClass('hidden'); + filter.one('#amosfilter_fver #flast').set('checked', true); + }); + } var fcmpselectall = filter.one('#amosfilter_fcmp_actions_all'); fcmpselectall.on('click', function(e) { // Check all displayed components. @@ -106,17 +108,19 @@ YUI.add('moodle-local_amos-filter', function(Y) { }); var flast = filter.one('#flast'); - flast.on('change', function(e) { - e.preventDefault(); - filter.all('.fver').set('disabled', e.currentTarget.get('checked')); - if (e.currentTarget.get('checked')) { - filter.one('#amosfilter_fver_versions').addClass('hidden'); - filter.all('#amosfilter_fcmp').addClass('hiddenversions'); - } else { - filter.one('#amosfilter_fver_versions').removeClass('hidden'); - filter.all('#amosfilter_fcmp').removeClass('hiddenversions'); - } - }); + if (flast) { + flast.on('change', function (e) { + e.preventDefault(); + filter.all('.fver').set('disabled', e.currentTarget.get('checked')); + if (e.currentTarget.get('checked')) { + filter.one('#amosfilter_fver_versions').addClass('hidden'); + filter.all('#amosfilter_fcmp').addClass('hiddenversions'); + } else { + filter.one('#amosfilter_fver_versions').removeClass('hidden'); + filter.all('#amosfilter_fcmp').removeClass('hiddenversions'); + } + }); + } // search for components var fcmpsearch = filter.one('#amosfilter_fcmp_actions_search'); @@ -138,16 +142,18 @@ YUI.add('moodle-local_amos-filter', function(Y) { // make greylist related checkboxed mutally exclusive var fglo = filter.one('#amosfilter_fglo'); var fwog = filter.one('#amosfilter_fwog'); - fglo.on('change', function(e) { - if (fglo.get('checked')) { - fwog.set('checked', false); - } - }); - fwog.on('change', function(e) { - if (fwog.get('checked')) { - fglo.set('checked', false); - } - }); + if (fglo && fwog) { + fglo.on('change', function (e) { + if (fglo.get('checked')) { + fwog.set('checked', false); + } + }); + fwog.on('change', function (e) { + if (fwog.get('checked')) { + fglo.set('checked', false); + } + }); + } // display the "loading" icon after the filter button is pressed var fform = Y.one('#amosfilter_form');