Calendar Event Manager

chadscott
Community Contributor
11
7523

Are you tired of seeing all those ancient events from two years ago in your syllabus feed? Want an easier way to remove old events without having to manually delete each one or having to remove all dates from everything in the course? Give this script a try! You can download the script and use Tamper Monkey in Chrome or other userscript tool for personal use or copy the script into the custom javascript file to give access to all instructors on your campus!

This is a basic version of the script and I'd like to some help adding pagination to the events listed (it's limited to 100 by the API). Let me know what you think!

Calendar Event Manager Script

Canvas Calendar

Manage Course Events

11 Comments
dgrobani
Community Champion

Nice! I don't have a use for this but I expect someone will, and I enjoyed reading your code.

On the pagination question, have you looked at Pagination - Canvas LMS REST API Documentation? I've implemented pagination in Python but not in JavaScript, but  @James  provided pagination JavaScript code in this discussion: Handling Pagination.

James
Community Champion

I wrote that a long time ago and have found there are more efficient ways with asynchronous calls made by JavaScript. It's not bad for a few calls, but I did some testing with loading 3000+ courses and it took a while. Somewhere in the widely dispersed code I've written, there is a place where I use the last page instead of the next page and then automatically generate all of the URLs that it would call and then let the browser queue them 5 or 6 at a time. It's probably buried in something. I think the last time I looked at it was back in June when I was testing some calls for cesbrandt's Full Course Listing with Sorting and Filtering . It's much faster than waiting for one to finish before grabbing the next. But like I said, it won't matter much with just a few calls.

cesbrandt
Community Champion

 @James  What can I say? I'm paranoid about it all getting done. xD

dgrobani and  @chadscott , the functions that James is referring to from the course listing/sorter/filterer do exactly what he described with one exception: it sends all the requests for the pagination at the same time. The browser then dictates how many requests get processed at any given time. These functions were my attempt at "modernizing" my style, which is normally much more cautious in its execution. They will also combine all the arrays of results from the pagination into a single, massive array to be worked with as if the pagination didn't exist. You're welcome to use them if you'd like.

Note: The callAPI function uses the isEmpty function. I just made it a separate function for its potential use as a standalone function.

/**
* @name          Is Object or Array Empty?
* @description   Generic function for determing if a JavaScript object or array
*                is empty
* @return bool   Yes, it is empty; No, it is not empty
*/

function isEmpty(obj) {
     if(Object.prototype.toString.call(obj) == '[object Array]') {
          return obj.length > 0 ? false : true;
     } else {
          for(var key in obj) {
               if(obj.hasOwnProperty(key)) {
                    return false;
               }
          }
          return true;
     }
}

/**
* @name          API Call
* @description   Calls the Canvas API
* @return undefined
*/

function callAPI(context, page, getVars, oncomplete, oncompleteInput, lastPage,
                      firstCall) {
     var validContext = Object.prototype.toString.call(context);
     var callURL = url.match(/^.*(?:instructure\.com)/)[0] + '/api/v1';
     if(validContext) {
          context.forEach(function(contextLevel) {
               callURL += '/' + contextLevel;
          });
     }
     var audit = callURL.match(/audit/) !== null ? true : false;
     getVars = getVars === null ? [{}] : getVars;
     oncompleteInput = oncompleteInput === null ? function(output) {
          console.log(output);
     } : oncompleteInput;
     firstCall = typeof firstCall != 'undefined' ? firstCall : [{}];
     var expandedVars = getVars.slice(0);
     page = typeof page === 'undefined' ? (audit ? 'first' : 1) : page;
     expandedVars[0].page = page;
     expandedVars[0].per_page = 100;
     var compiledJSON = [];
     var callsToMake = [{
          callURL: callURL,
          data: $.extend(true, {}, expandedVars[0])
     }];
     if(page === 1 || audit) {
          $.when(callAJAX(callURL, expandedVars[0])).then(function(json, status,
                                                                                  xhr) {
               if(!audit) {
                    if(xhr.getResponseHeader('Link') !== null) {
                         var lastPage = parseInt(xhr.getResponseHeader('Link').match(
                              /\bpage=(\d+\b)(?=[^>]*>; rel="last")/)[1]);
                         if(page < lastPage) {
                              callAPI(context, (page + 1), getVars, oncomplete,
                                        oncompleteInput, lastPage, json);
                         } else {
                              oncomplete(json, oncompleteInput);
                         }
                    } else {
                         oncomplete(json, oncompleteInput);
                    }
               } else {
                    if(xhr.status != 200) {
                         oncomplete({
                              error: 'There was an error. Please try again.'
                         });
                    } else {
                         var results = (firstCall.length == 1 &&
                                           isEmpty(firstCall[0])) ? json.events :
                                             $.merge(firstCall, json.events);
                         if(json.events.length === 100) {
                              page = xhr.getResponseHeader('link').match(
                                   /\bpage=[^&]*(?=[^>]*>; rel="next")/)[0]
                                   .split('=')[1];
                              callAPI(context, page, getVars, oncomplete,
                                        oncompleteInput, null, results);
                         } else {
                              oncomplete(results, oncompleteInput);
                         }
                    }
               }
          });
     } else {
          for(var i = (page + 1), j = (callsToMake.length - 1); i <= lastPage;
               i++) {
               $.merge(callsToMake, [$.extend(true, {}, callsToMake[j])]);
               callsToMake[j].data.page = i;
          }
          var allCalls = callsToMake.map((currentSettings) => {
               return callAJAX(currentSettings.callURL, currentSettings.data);
          });
          $.when.apply($, allCalls).then(function() {
               $.each(arguments, function(index, value) {
                    if($.isArray(value[0])) {
                         $.merge(firstCall, value[0]);
                    }
               });
               oncomplete(firstCall, oncompleteInput);
          });
     }
     return;
}

/**
* @name          AJAX Call
* @description   Calls the the specified URL with supplied data
* @return obj    Full AJAX call is returned for processing elsewhere
*/

function callAJAX(callURL, data) {
     return $.ajax({
          url: callURL,
          data: data
     });
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I wrote them to make calling the API flexible.

callAPI(context, page, getVars, oncomplete, oncompleteInput, lastPage, firstCall);‍‍‍
VariableTypeDescriptionExample
contextarrayThis variable is the API URL after the /api/v1/ path. It can be broken down by level or supplied as a single, normal value. Breaking the pathing down allows for the use of dynamic values (i.e., recursively executing the call for all courses in an account).

['accounts', 1, 'subaccounts']

['accounts/1/subaccounts']

['accounts', account_id, 'subaccounts']

pageintThis variable dictates the pagination start point. Default: 11
getVarsarray/objectThis is a little complicated in that you supply an object of the GET variables within an array. There was a specific reason I did it this way instead of just supplying the object, but it escapes me. Important Note: This does NOT include the per_page or page variables.[{recursive: true}]
oncompletefunctionWhat will be done with the results?

function(subaccounts) { ... }

oncompleteInputAnyThis is a catch all variable for anything you wish to have made available to the oncomplete function...., function(subaccounts, extraData) { ... }, {valid: false}
lastPageintThis is calculated by the callAPI function and should not be supplied.
firstCallarrayThis is calculated by the callAPI function and should not be supplied.

Here's an example of a full call:

callAPI(['accounts', 1, 'subaccounts'], 1, [{recursive: true}], function(subaccounts, extraData) { ... }, {valid: false});‍‍‍‍
James
Community Champion

Thanks, cesbrandt, but I wasn't referring to your code as much as putting a time frame on when I had last looked at the situation Smiley Sad  The conversations we had urged me to incorporate it into my older code, but I never got around to that. But still -- thanks for throwing it out there, mine isn't complete and needs pulled together from a bunch of different scripts, so kudos to the man who has his act together Smiley Happy

My code is much simpler (less robust?), doesn't have instructure.com hard-coded into it, nor the requirement that 100 be returned to see if there's more (since there's no requirement that all API calls honor the 100 limit). 

What I did was to send one request, see how many pages should be returned by looking at the "Last" link (call this number n). This also handles adjusting the per_page value if needed. Then I would use a for loop to generate all of the pages from 2 to n and make the ajax calls. I have a global variable called pending that gets incremented for each call made and decremented once the call returns. I don't handle re-fetching information from calls that fail, but Christopher does.

What both Christopher and I are doing, which may not be the best thing to do in every case, is to load all of the data before we start processing it. In some cases, there is nothing to say that you can't process it as it comes in.  In the case of deleting events as  @chadscott ‌ with this script, I think that I would go ahead and keep with that -- I haven't tested it, but I have to wonder if deleting items while still loading them might throw off the count.

Also, Chad, while looking at the code just now, I noticed a typo on line 49. include[] has a couple of letters turned around.

var url = "/api/v1/users/self/courses?inlcude[]=term&per_page=75"; // change self to specific user number for testing

Other comments.

I know you have based some of your stuff off what I've written. I'm at the point where I'm moving away from jQuery to pure JavaScript as a way to help me learn JavaScript and the way it interacts. Much like the first programming I ever did was in machine language on an Heathkit ET3400A trainer with a 6 digit LED display and a hexadecimal keypad. If you get down to that low of a level, you have to understand how everything fits together. It helps me understand how everything fits together and is supposed to work so if someone comes along with a different language, I can often piece together how it should work.  I'm now at the point where I try to do everything in pure JavaScript except for really short things for other people or AJAX calls.

That said, it sure makes your code a lot longer and takes a lot more time to code and is harder to follow. Not that you're seeking or need my blessing, but it's okay if you want to stick with jQuery. If Canvas ever stops including it, you can always add it as a require in the user script and have it. But write something that you can understand (I still haven't fully digested Christopher's API stuff, but he's in good company because I don't understand much of what Canvas does with their JavaScript either).

My understanding of the jQuery ajax call is that the data property can handle an object. I see you're calling JSON.stringify, which works, but I don't think it's necessary. That's another reason why I stick with jQuery for making those AJAX calls, little things like that are so nice.

I'm not sure that I would rely on the last role being teacher or admin. I don't think it's specified anywhere the order that it appears in so it would be better to check using the indexOf() method like dgrobani‌ does in his stuff. I may not be remembering it exactly, but I think it goes something like this:

if (ENV.current_user_roles.indexOf('teacher') > -1 || ENV.current_user_roles.indexOf('admin') > -1) { // do something }

Anyway, since Christopher threw his out here, I went and looked up the thing I was using to benchmark. It's rough, but works. The point is to load all of the course in an institution and play around with different per_page settings to see if it makes a difference. That's what all the timing data stuff is about. It was also designed to look at the rate limiting stuff to see if there was a problem with making parallel calls to the API. I haven't included that in any of the other stuff I've done and it seems to not be a problem with calls made through the browser, which limits you to 5-6 parallel calls, but it could be a problem if you spun up 50 processes to fetch information.

Oh yeah, Christopher finds time to document things, which is a huge downside to anything I write.

(function() {
  'use strict';

  var perPage = 100;
  var maxPages = 6000;
  var pending;
  var options = {
    'per_page' : perPage
  };
  var timings = [];
  var courses = [];

  try {
    addTimingData();

    var url = '/api/v1/accounts/self/courses';
    $.ajax({
      'url' : url,
      'dataType' : 'json',
      'data' : options,
    }).done(function(data, textStatus, jqXHR) {
      addTimingData(data, textStatus, jqXHR);
      var lastPageNumber = getLastPageNumber(jqXHR.getResponseHeader('Link'));
      if (lastPageNumber !== false && lastPageNumber > 1) {
        getAdditionalPages(lastPageNumber);
      }
    }).fail(ajaxFail);
  } catch (e) {
    console.log(e);
  }

  function getAdditionalPages(lastPageNumber) {
    if (lastPageNumber) {
      var lastPage = lastPageNumber < maxPages ? lastPageNumber : maxPages;
      pending = lastPage - 1;
      for (var i = 2; i <= lastPage; i++) {
        options.page = i;
        $.ajax({
          'url' : url,
          'dataType' : 'json',
          'data' : options,
        }).done(ajaxDone).fail(ajaxFail);
      }
    }
  }

  function ajaxDone(data, textStatus, jqXHR) {
    addTimingData(data, textStatus, jqXHR);
    checkFinished();
  }

  function ajaxFail(jqXHR, textStatus, errorThrown) {
    addTimingData(null, textStatus, jqXHR);
    console.log(errorThrown.e + ' : ' + errorThrown.message);
    checkFinished();
  }

  function checkFinished() {
    if (typeof pending !== 'undefined') {
      pending--;
      if (pending <= 0) {
        finished();
      }
    }
  }

  function finished() {
    console.log(timings);
    console.log(JSON.stringify(timings));
  }

  function getLastPageNumber(linkTxt) {
    var pageRegEx = new RegExp('(?:[?&])page=([0-9]+)', 'i');
    var perPageRegEx = new RegExp('(?:[?&])per_page=([0-9]+)', 'i');
    var lastRegEx = new RegExp('^<(.*)>; rel="last"$');
    var lastPageNumber = false;
    if (linkTxt) {
      var links = linkTxt.split(',');
      for (var i = 0; i < links.length; i++) {
        var matches = lastRegEx.exec(links[i]);
        if (matches) {
          var url = matches[1];
          var pageMatch = pageRegEx.exec(url);
          if (pageMatch) {
            lastPageNumber = parseInt(pageMatch[1]);
          }
          var perPageMatch = perPageRegEx.exec(url);
          if (perPageMatch) {
            options.per_page = perPageMatch[1];
          }
        }
      }
    }
    return lastPageNumber;
  }

  function getHeaderLinks(linkTxt) {
    if (typeof linkTxt === 'undefined' || !linkTxt) {
      return null;
    }
    var linkInfo = [];
    var pageRegEx = new RegExp('(?:[?&])page=([0-9]+)', 'i');
    var perPageRegEx = new RegExp('(?:[?&])per_page=([0-9]+)', 'i');
    var linkRegEx = new RegExp('^<(.*)>; rel="(current|first|next|last)"$', 'i');
    var links = linkTxt.split(',');
    for (var i = 0; i < links.length; i++) {
      var matches = linkRegEx.exec(links[i]);
      if (matches) {
        var url = matches[1];
        var rel = matches[2].toLowerCase();
        var perPage = null;
        var page = null;
        var pageMatch = pageRegEx.exec(url);
        if (pageMatch) {
          page = Number(pageMatch[1]);
        }
        var perPageMatch = perPageRegEx.exec(url);
        if (perPageMatch) {
          perPage = Number(perPageMatch[1]);
        }
        linkInfo[rel] = {
          'url' : url,
          'page' : page,
          'perPage' : perPage
        };
      }
    }
    return linkInfo;
  }

  function addTimingData(data, textStatus, jqXHR) {
    var jqFields = {
      'date' : 'Date',
      'limit' : 'X-Rate-Limit-Remaining',
      'cost' : 'X-Request-Cost',
      'runtime' : 'X-Runtime',
    };
    var now = new Date();
    var obj = {
      'timestamp' : now.toUTCString()
    };
    if (typeof data !== 'undefined' && data !== null) {
      obj.contentLength = data.length;
      if (Array.isArray(data)) {
        for (var j = 0; j < data.length; j++) {
          courses.push(data[j]);
        }
      }
    }
    if (typeof textStatus !== 'undefined') {
      obj.status = textStatus;
    }
    if (typeof jqXHR !== 'undefined') {
      var jqKeys = Object.keys(jqFields);
      for (var i = 0; i < jqKeys.length; i++) {
        var key = jqKeys[i];
        var headerKey = jqFields[key];
        var val = jqXHR.getResponseHeader(headerKey);
        if (typeof val !== 'undefined') {
          obj[key] = val;
        }
      }
    }
    timings.push(obj);
    return;
  }

})();
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Finally, Chad, for something simple like what you're doing, you don't need anything as elaborate as what Christopher and I are throwing out to handle the pagination. Your event data is probably much smaller and so there's no need to load in parallel. There are probably libraries out there that would do all of this for us and save us all a bunch of time - but they wouldn't be as much fun.

cesbrandt
Community Champion

James Jones wrote:

My code is much simpler (less robust?), doesn't have instructure.com hard-coded into it, nor the requirement that 100 be returned to see if there's more (since there's no requirement that all API calls honor the 100 limit). 

Adjusting it to use a domain variable instead of instructure.com isn't a difficult task, I'm just too lazy to do it. ^^'

James Jones wrote:

What I did was to send one request, see how many pages should be returned by looking at the "Last" link (call this number n). This also handles adjusting the per_page value if needed. Then I would use a for loop to generate all of the pages from 2 to n and make the ajax calls. I have a global variable called pending that gets incremented for each call made and decremented once the call returns. I don't handle re-fetching information from calls that fail, but Christopher does.

Brilliant thing is, that's exactly how I first handled them! I'd use a setInterval and check for the global variable to read at 0 before it could execute the follow-up code. However, I found that in rare cases, such as massive calls where the browser may hiccup due to the amount of memory needed to support the process, the x calls may be started before the browser glitches, preventing more for starting untill those that began finish. This would cause the variable to hit 0 and start the followup while more calls were still needed.

James Jones wrote:

I know you have based some of your stuff off what I've written. I'm at the point where I'm moving away from jQuery to pure JavaScript as a way to help me learn JavaScript and the way it interacts. Much like the first programming I ever did was in machine language on an Heathkit ET3400A trainer with a 6 digit LED display and a hexadecimal keypad. If you get down to that low of a level, you have to understand how everything fits together. It helps me understand how everything fits together and is supposed to work so if someone comes along with a different language, I can often piece together how it should work.  I'm now at the point where I try to do everything in pure JavaScript except for really short things for other people or AJAX calls.

I did the exact opposite, refusing to use jQuery until I was confident in my ability to write in proper JavaScript. Now, I prefer jQuery for the simple fact that it's easier, as what the intent of its creation.

Also, if you'd like, I believe I have a JavaScript function for Canvas API calls somewhere. I know I had originally written one, but the needed functionality of my scripts expanded so much that it eventually became too much work to bring it upto spec when a jQuery equivalent would be much easier.

James Jones wrote:

That said, it sure makes your code a lot longer and takes a lot more time to code and is harder to follow. Not that you're seeking or need my blessing, but it's okay if you want to stick with jQuery. If Canvas ever stops including it, you can always add it as a require in the user script and have it. But write something that you can understand (I still haven't fully digested Christopher's API stuff, but he's in good company because I don't understand much of what Canvas does with their JavaScript either).

 @chadscott , pay attention to this! James make s a VERY important point here for all programmers/scripters: write something that you can understand! Yes, it's important that another programmer be able to come in behind you and understand what your work does, but it's more important that you understand it. Every script/program has minimum requirements that define what knowledge is needed to properly understand the code, but these can't be identified if the author doesn't understand what they wrote. I learned a lot just writing those functions, both about JavaScript and about the Canvas API.

 @James ‌, don't be too concerned about not understanding the API functions in their entirety, I had help with writing some of it and learned quite a bit from doing so. It's actually a blend of jQuery and pure JavaScript, along with quite a bit of complex logic to cater to Canvas (i.e., the page variable for the audit API isn't an integer). As for the JavaScript of Canvas, it heavily relies on anonymous functions which makes the code quite a bit more difficult to understand, in my opinion. It also makes it impossible to execute many of the functions a second time for custom functionality utilizing official functionality.

James Jones wrote:

Oh yeah, Christopher finds time to document things, which is a huge downside to anything I write.

This is a lie! If I commented my code, I wouldn't bother with writing explanations when I post it! xD

James Jones wrote:

Finally, Chad, for something simple like what you're doing, you don't need anything as elaborate as what Christopher and I are throwing out to handle the pagination. Your event data is probably much smaller and so there's no need to load in parallel. There are probably libraries out there that would do all of this for us and save us all a bunch of time - but they wouldn't be as much fun.

 @chadscott ‌, yet another great point James has made that I don't see from a lot of scripters/programmers: don't make something more complicated than it needs to be. The code I used in the course list/sorter/filterer script is too elaborate for what is actually needed. I won't deny that, but there's a reason it's so much more elaborate than necessary that is also a great point for scripters/programmers to follow: don't reinvent something unless you are actually improving it. However, that point is a case-by-case evaluation. Sometimes reinventing the function to eliminate the excess will be beneficial enough to warrant the effort spent on it, but that's not something you'll actually be able to judge until the work is done. As the author of a script, you have to rely on experience and knowledge of what you're working with to estimate if the benefit of writing a simpler function outweigh the cost of doing so.

The functions I used and provided were extracted from a significantly more complex script that offers dozens of additional functions across Canvas, many of which require the use of API calls in a more elaborate manner. It worked for the intended functionality and, while strictly capable of more than was needed, provided the exact functionality desired without having to rewrite the logic to remove that functionality. This combined with my work I do made it a quick decision to not write a simpler version (or, having expanded it from a simpler version, finding the older version among hundreds of thousands of files).

James
Community Champion

I'm responding to Christopher's message, but the content is also intended for the broader audience.

Adjusting it to use a domain variable instead of instructure.com isn't a difficult task, I'm just too lazy to do it. ^^'

I think we've discussed this before, but it's not necessary either. I know, it's one of those places where we tend to over-engineer things. But if it's running inside a browser, then you can just use a relative URL without the hostname. It might also be one of those things I didn't know about like non-integer page numbers for the audit endpoint.

This is a lie! If I commented my code, I wouldn't bother with writing explanations when I post it! xD

You at least have JSDoc comments before your functions. That counts as documentation in my book. Actually, I think the explanations that we both provide when share the code counts as documentation, it's just not inside the code.

The functions I used and provided were extracted from a significantly more complex script that offers dozens of additional functions across Canvas, many of which require the use of API calls in a more elaborate manner.

This goes to the point of using a library, which is generally a good idea if you have one. The library is typically written to handle all of the cases to make it easier on the person using it. It is longer and more complicated. It may have hundreds of features and the application developer may only need a couple of them. No one would consider yanking one or two functions out of the code, they would just include the whole library. Thankfully, the cost to include a library with user scripts is minimal. The user script manager downloads a copy when the script is installed and then it fetches it from the user's machine, not the Internet, every time the script is ran. The can be very effective and save a lot of time developing things and like Christopher said, don't reinvent the wheel unless you're making it better.

So why do I try not to use them?


It's not because I'm a moron.

Don't touch that one.

I try not to use them because I hold out the possibility that my code might get included into the Custom JavaScript and not ran as user script. I don't design them that way, but I consider it might happen. I'm not just talking the simple stuff, someone said they were taking QuizWiz: Enhancements to SpeedGrader and Quizzes‌, which is by far my most complicated script, and making it available to all of their nursing faculty. So all of my code (at least that written after I figured it out) checks to see what page they're running on inside Canvas. That's part of the \\ @include line as a user script, so it's redundant for those, but it's still a good idea. But I also try to minimize the number of libraries that are required so someone doesn't have to load a bunch of stuff to use it.  I know there are libraries that will load libraries for you. but they take extra time and I want the page to be responsive and quick - if possible (translated: I don't know how to use them).

If Christopher's code was in a library so it was included with a single line, no one would probably bat an eye or think twice about it -- "it works! yay! thank you! on to my stuff!"

For him, it would be stupid to take working code that has been tested and try to remove all of the stuff that isn't needed for a particular application. That's a good way to break something, just to save a few lines of code.

I think the benefit is sometimes lost when we (he and I both do this) share it as a standalone function instead of calling it a library. Then it looks ominous and everyone else is left wondering why I need to include these 100 lines of code to do something when I only had 20 lines in the original program.

We understand why we do it. It's not a library to us, it's just a block of code that we keep reusing and copy/pasting from one script to another. To us, it is a function. And to some extent that happens with user scripts. You have a single JavaScript file to work with.  You can require other scripts, packages, or libraries, but if you want to make everything self-contained, the library turns into just a function in a file.

Chad originally asked about pagination and the links that Daniel provided are probably sufficient for his needs. We've been going off on an intellectual safari here, which is loads of fun, but I don't want him to think that he needs to do either of what we've done to make his script work.

The really simple (but not as user friendly) solution, assuming Canvas doesn't return deleted items when you request them, is to just click the button several times until they're all gone. Yeah, I wouldn't choose that option, either.

James
Community Champion

I think your closeness to the situation led you to misinterpret what he wrote. I don't see where he said you didn't understand it and I certainly didn't take it that way when I read it. I think he was stressing the importance of making sure that you understand it. It's sound advice for all programmers, which is kind of where the discussion headed. In fact, he's started another discussion on the differences in how people code, based off of what were talking about here: https://community.canvaslms.com/thread/18233-developer-styles 

cesbrandt
Community Champion

I do apologize if you took my words as accusing you of not understanding your work. That was not my intention in any way. As James has noted interpreting it, I only intended to express my wholehearted agreement with James that coders should understand what they write over using something they don't understand simply because it gives them the result they want.

Again, I am sorry for any misunderstanding it may have caused.

bthankful
Community Member

If I'm not a Canvas admin, just a faculty using Canvas, am I supposed to run chadscott's script in Chrome and delete multiple calendar events in my courses?

Thanks!

GeoffreyWheeler
Community Participant

I searched add-ons.  I found tampermonkey and added it to my browser.  I clicked on the link, "Calendar Event Manager Script" in your article. I refreshed my calendar page and your magic button, "Manage Course Events" appeared underneath, "Calendar Feed".  In Firefox I clicked on it and the button outline turned dark blue.  Nothing else perceptible happened. In Chrome, it worked the first time bringing up the dialog box as shown in your article. I checked the box to delete all events and I think some were deleted but most if not all were left. The button would not work a second time in Chrome even with a restart of Chrome. Thanks for putting the time into coding and sharing it.  I hope that you can show me how to make it work. 

Firefox:

I found that the dialog box would appear across the bottom of the window. The Canvas menu on the left covered the check boxes. I found that [Ctrl][Shift]M opened it in a responsive (smart phone) mode and by narrowing the window, could make the check boxes visible. I could not get to the "submit" button.  When I went and tried it in Chrome again, the same thing happened: the Event Manager appeared across the bottom of the window, I could reach it in a responsive developer tool mode but could not access the "Submit". button.

dGillman
Community Novice

@GeoffreyWheeler: Clearing my cache made the script work again. (Something I read said to do that.)