MDL-20534 lti: Updating privacy settings dynamically based on matched Launch URL.
[moodle.git] / mod / lti / mod_form.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Javascript extensions for the External Tool activity editor.
18  *
19  * @package    mod
20  * @subpackage lti
21  * @copyright  Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com)
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 (function(){
25     var Y;
27     M.mod_lti = M.mod_lti || {};
29     M.mod_lti.LTI_SETTING_NEVER = 0;
30     M.mod_lti.LTI_SETTING_ALWAYS = 1;
31     M.mod_lti.LTI_SETTING_DELEGATE = 2;
33     M.mod_lti.editor = {
34         init: function(yui3, settings){
35             if(yui3){
36                 Y = yui3;
37             }
39             var self = this;
40             this.settings = Y.JSON.parse(settings);
42             this.urlCache = {};
44             this.addOptGroups();
46             var updateToolMatches = function(){
47                 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
48                 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
49             };
51             var typeSelector = Y.one('#id_typeid');
52             typeSelector.on('change', function(e){
53                 updateToolMatches();
55                 self.toggleEditButtons();
56             });
58             this.createTypeEditorButtons();
60             this.toggleEditButtons();
62             var textAreas = new Y.NodeList([
63                 Y.one('#id_toolurl'),
64                 Y.one('#id_securetoolurl'),
65                 Y.one('#id_resourcekey'),
66                 Y.one('#id_password')
67             ]);
69             var debounce;
70             textAreas.on('keyup', function(e){
71                 clearTimeout(debounce);
73                 //If no more changes within 2 seconds, look up the matching tool URL
74                 debounce = setTimeout(function(){
75                     updateToolMatches();
76                 }, 2000);
77             });
79             updateToolMatches();
80         },
82         clearToolCache: function(){
83             this.urlCache = {};
84         },
86         updateAutomaticToolMatch: function(field){
87             var self = this;
89             var toolurl = field;
90             var typeSelector = Y.one('#id_typeid');
92             var id = field.get('id') + '_lti_automatch_tool';
93             var automatchToolDisplay = Y.one('#' + id);
95             if(!automatchToolDisplay){
96                 automatchToolDisplay = Y.Node.create('<span />')
97                                         .set('id', id)
98                                         .setStyle('padding-left', '1em');
100                 toolurl.insert(automatchToolDisplay, 'after');
101             }
103             var url = toolurl.get('value');
105             //Hide the display if the url box is empty
106             if(!url){
107                 automatchToolDisplay.setStyle('display', 'none');
108             } else {
109                 automatchToolDisplay.set('innerHTML', '');
110                 automatchToolDisplay.setStyle('display', '');
111             }
113             var selectedToolType = typeSelector.get('value');
114             var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
116             //A specific tool type is selected (not "auto")
117             //We still need to check with the server to get privacy settings
118             if(selectedToolType > 0){
119                 //If the entered domain matches the domain of the tool configuration...
120                 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
121                 var match = domainRegex.exec(url);
122                 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
123                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.using_tool_configuration + selectedOption.get('text'));
124                 } else {
125                     //The entered URL does not match the domain of the tool configuration
126                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.str.lti.domain_mismatch);
127                 }
128             }
130             var key = Y.one('#id_resourcekey');
131             var secret = Y.one('#id_password');
133             //Indicate the tool is manually configured
134             //We still check the Launch URL with the server as course/site tools may override privacy settings
135             if(key.get('value') !== '' && secret.get('value') !== ''){
136                 automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.custom_config);
137             } 
138             
139             var continuation = function(toolInfo){
140                 self.updatePrivacySettings(toolInfo);
142                 if(toolInfo.toolname){
143                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.using_tool_configuration + toolInfo.toolname);
144                 } else {
145                     //Inform them custom configuration is in use
146                     if(key.get('value') === '' || secret.get('value') === ''){
147                         automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.str.lti.tool_config_not_found);
148                     }
149                 }
150             };
152             //Cache urls which have already been checked to increase performance
153             if(self.urlCache[url]){
154                 continuation(self.urlCache[url]);
155             } else {
156                 self.findToolByUrl(url, function(toolInfo){
157                     if(toolInfo){
158                         self.urlCache[url] = toolInfo;
160                         continuation(toolInfo);
161                     }
162                 });
163             }
164         },
166         /**
167          * Updates display of privacy settings to show course / site tool configuration settings.
168          */
169         updatePrivacySettings: function(toolInfo){
170             if(!toolInfo || !toolInfo.toolname){
171                 toolInfo = {
172                     sendname: M.mod_lti.LTI_SETTING_DELEGATE,
173                     sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
174                     acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE,
175                     allowroster: M.mod_lti.LTI_SETTING_DELEGATE
176                 }
177             }
178             
179             var setting, control;
180             
181             //Can't look these up by ID as they seem to get random IDs. 
182             //Setting an id manually from mod_form made them turn into text boxes.
183             var privacyControls = {
184                 sendname: Y.one('input[name=instructorchoicesendname]'),
185                 sendemailaddr: Y.one('input[name=instructorchoicesendemailaddr]'),
186                 acceptgrades: Y.one('input[name=instructorchoiceacceptgrades]'),
187                 allowroster: Y.one('input[name=instructorchoiceallowroster]')
188             };
189             
190             //Store a copy of user entered privacy settings as we may overwrite them
191             if(!this.userPrivacySettings){
192                 this.userPrivacySettings = {};
193             }
195             for(setting in privacyControls){
196                 if(privacyControls.hasOwnProperty(setting)){
197                     control = privacyControls[setting];
199                     //Only store the value if it hasn't been forced by the editor
200                     if(!control.get('disabled')){
201                         this.userPrivacySettings[setting] = control.get('checked');
202                     }
203                 }
204             }
205             
206             //Update UI based on course / site tool configuration
207             for(setting in privacyControls){
208                 if(privacyControls.hasOwnProperty(setting)){
209                     var settingValue = toolInfo[setting];
210                     control = privacyControls[setting];
211                     
212                     if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
213                         control.set('disabled', true);
214                         control.set('checked', false);
215                         control.set('title', M.str.lti.forced_help);
216                     } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
217                         control.set('disabled', true);
218                         control.set('checked', true);
219                         control.set('title', M.str.lti.forced_help);
220                     } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
221                         control.set('disabled', false);
222                         
223                         //Get the value out of the stored copy
224                         control.set('checked', this.userPrivacySettings[setting]); 
225                         control.set('title', '');
226                     }
227                 }
228             }
229         },
231         getSelectedToolTypeOption: function(){
232             var typeSelector = Y.one('#id_typeid');
234             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
235         },
237         /**
238          * Separate tool listing into option groups. Server-side select control
239          * doesn't seem to support this.
240          */
241         addOptGroups: function(){
242             var typeSelector = Y.one('#id_typeid');
244             if(typeSelector.one('option[courseTool=1]')){
245                 //One ore more course tools exist
247                 var globalGroup = Y.Node.create('<optgroup />')
248                                     .set('id', 'global_tool_group')
249                                     .set('label', M.str.lti.global_tool_types);
251                 var courseGroup = Y.Node.create('<optgroup />')
252                                     .set('id', 'course_tool_group')
253                                     .set('label', M.str.lti.course_tool_types);
255                 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
256                     globalGroup.append(node);
257                 });
259                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
260                     courseGroup.append(node);
261                 });
263                 if(globalOptions.size() > 0){
264                     typeSelector.append(globalGroup);
265                 }
267                 if(courseOptions.size() > 0){
268                     typeSelector.append(courseGroup);
269                 }
270             }
271         },
273         /**
274          * Adds buttons for creating, editing, and deleting tool types.
275          * Javascript is a requirement to edit course level tools at this point.
276          */
277         createTypeEditorButtons: function(){
278             var self = this;
280             var typeSelector = Y.one('#id_typeid');
282             var createIcon = function(id, tooltip, iconUrl){
283                 return Y.Node.create('<a />')
284                         .set('id', id)
285                         .set('title', tooltip)
286                         .setStyle('margin-left', '.5em')
287                         .set('href', 'javascript:void(0);')
288                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
289             }
291             var addIcon = createIcon('lti_add_tool_type', M.str.lti.addtype, this.settings.add_icon_url);
292             var editIcon = createIcon('lti_edit_tool_type', M.str.lti.edittype, this.settings.edit_icon_url);
293             var deleteIcon  = createIcon('lti_delete_tool_type', M.str.lti.deletetype, this.settings.delete_icon_url);
295             editIcon.on('click', function(e){
296                 var toolTypeId = typeSelector.get('value');
298                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
299                     window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
300                 } else {
301                     alert(M.str.lti.cannot_edit);
302                 }
303             });
305             addIcon.on('click', function(e){
306                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
307             });
309             deleteIcon.on('click', function(e){
310                 var toolTypeId = typeSelector.get('value');
312                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
313                     if(confirm(M.str.lti.delete_confirmation)){
314                         self.deleteTool(toolTypeId);
315                     }
316                 } else {
317                     alert(M.str.lti.cannot_delete);
318                 }
319             });
321             typeSelector.insert(addIcon, 'after');
322             addIcon.insert(editIcon, 'after');
323             editIcon.insert(deleteIcon, 'after');
324         },
326         toggleEditButtons: function(){
327             var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
328             var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
330             //Make the edit / delete icons look enabled / disabled.
331             //Does not work in older browsers, but alerts will catch those cases.
332             if(this.getSelectedToolTypeOption().getAttribute('editable')){
333                 lti_edit_tool_type.setStyle('opacity', '1');
334                 lti_delete_tool_type.setStyle('opacity', '1');
335             } else {
336                 lti_edit_tool_type.setStyle('opacity', '.2');
337                 lti_delete_tool_type.setStyle('opacity', '.2');
338             }
339         },
341         addToolType: function(toolType){
342             var typeSelector = Y.one('#id_typeid');
343             var course_tool_group = Y.one('#course_tool_group');
345             var option = Y.Node.create('<option />')
346                             .set('text', toolType.name)
347                             .set('value', toolType.id)
348                             .set('selected', 'selected')
349                             .setAttribute('editable', '1')
350                             .setAttribute('courseTool', '1')
351                             .setAttribute('domain', toolType.tooldomain);
353             if(course_tool_group){
354                 course_tool_group.append(option);
355             } else {
356                 typeSelector.append(option);
357             }
359             //Adding the new tool may affect which tool gets matched automatically
360             this.clearToolCache();
361             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
362             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
363         },
365         updateToolType: function(toolType){
366             var typeSelector = Y.one('#id_typeid');
368             var option = typeSelector.one('option[value="' + toolType.id + '"]');
369             option.set('text', toolType.name)
370                   .set('domain', toolType.tooldomain);
372             //Editing the tool may affect which tool gets matched automatically
373             this.clearToolCache();
374             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
375             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
376         },
378         deleteTool: function(toolTypeId){
379             var self = this;
381             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
382                 on: {
383                     success: function(){
384                         self.getSelectedToolTypeOption().remove();
386                         //Editing the tool may affect which tool gets matched automatically
387                         self.clearToolCache();
388                         self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
389                         self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
390                     },
391                     failure: function(){
393                     }
394                 }
395             });
396         },
398         findToolByUrl: function(url, callback){
399             var self = this;
400             
401             if(!url || url === ''){
402                 return callback();
403             }
405             Y.io(self.settings.ajax_url, {
406                 data: {action: 'find_tool_config',
407                         course: self.settings.courseId,
408                         toolurl: url
409                 },
411                 on: {
412                     success: function(transactionid, xhr){
413                         var response = xhr.response;
415                         var toolInfo = Y.JSON.parse(response);
417                         callback(toolInfo);
418                     },
419                     failure: function(){
421                     }
422                 }
423             });
424         }
426     };
427 })();