James Jones

Hiding Content from Certain Roles

Blog Post created by James Jones Expert on Jun 13, 2017

Some people want to hide menu items from certain roles, while leaving it there for others. The ultimate solution is probably Canvas Permissions and Granularity Feature Ideas, but in the meantime there are some things you can do. This blog post in response to Remove "Export Course" from Settings, where Kevin Wright wanted to remove the "Export Course Content" link from the right sidebar, but only for TAs. Daniel Grobani provided some code and a warning that if you use custom JavaScript, you better understand what it does. This blog will explain some of what is going on behind the scenes.

 

While a specific request started this blog, the information contained here can be used to eliminate other things. It's intended to be more of a how to guide than a solution to just one problem.

 

Before we get started ...

 

Before I write up how to do this, I want to warn you that removing the link does not remove the ability for people to do what you don't want them to do. It only makes it harder. In this particular case, anyone with the proper permissions can use the Content Exports API to export the information.

 

Furthermore, this particular issue is also related to the ability for students to export course content as an ePub. Announced in the Canvas Production Release Notes (2017-04-01) is the ability to turn on Course Content Export for an entire account of subaccount. When this is done, if the teacher doesn't disable it in the course settings, then the students will see the ability to Export Course Content on the top of the home page for the course.

 

 

The question here wasn't about removing that link, but be aware that in general, hiding or removing things is likely to miss something.

 

Also realize that whatever you do here may not (probably won't) work in the mobile apps. If Canvas ever gets to where they're using ReactJS to generate pages and then do a render() command at some point after you've removed it, it will show back up. That means adding Mutation Observers to watch for the content to show back up. That's a different blog post completely.

 

The problem

Kevin's request was to get rid of the Export Course Content button.

 

Suggested Code

Here's the code that was suggested in that question, but the real emphasis is on the explanation of what it does and what you need to look for to do make it work.

if (document.URL.endsWith('settings') && ENV.current_user_roles.indexOf('ta') > -1) {
    $('.icon-download').parent().remove();
}

 

Prerequisite Information

The Course Settings page contains an Export Course Content link. That page URL looks something like this:

https://richland.instructure.com/courses/2151246/settings

 

When you get to that page, right click and choose Inspect Element (Firefox) or Inspect (Chrome)

 

When you do that, you should get the browser's inspector tool, which shows you the object from the Document Object Module (DOM). It should look something like this

That's not the whole picture, though, it's contained within a bigger object, the entire list of items, each of which represents an item in the menu.

 

That's hard to see all of it, but you can click on it to enlarge it, or here it is cropped so I can talk about things and have it visible.

 

The key is to come up with a CSS selector for the element you want to get rid of.

 

The other piece of information you need is what roles the current user has to make sure they're a TA and not something a teacher or administrator. This is contained in an variable that Canvas exposes called ENV. To get access to it, you need to be in the browser's developer tools, which you are if you inspected the element. If not, pressing F12 will get you there. Once there, click on the Console tab.

 

Type ENV into the input field and hit enter.

You will get a really long line that may look like this

 

 

You need to click the arrow next to the object to expand it. In Firefox, click on the word "Object". Then scroll down to the current_user_rolls item and click the arrow to expand it.

 

That's mine, but I'm not a TA in anything. Here's one for someone who is a TA.

 

Running on the right pages

The code checks to make sure that the URL ends with settings

document.URL.endsWith('settings')

The first part of the script makes sure that the page ends with 'settings'. This includes

  • Any course settings page, like /courses/2151246/settings
  • The account settings page, like /accounts/97773/settings
  • Any content page that ends in settings, like a page named 'Python Settings', which looks like /courses/896851/pages/python-settings
  • Any URL that contains a query parameter that ends in settings, like /courses/2151246/gradebook?show=settings
  • It will not run on a page like courses/2151246/settings/configurations. In case you're wondering how I got there, I went to the course settings page, click on Apps, clicked on View App Configurations. However, doing that didn't refresh the page and so it didn't trigger. However, if someone went there directly, it would show up.

 

Whoops! Okay, so don't name any of your pages to end in 'settings' and don't go to the account settings page as it's for admins only anyway. The last one probably won't happen, but you might have some other JavaScript (or someone else might write a script that does) that adds some features and looks at this, so it's best not to allow it.

 

A better way would be to check and make sure that you're on a course settings page. This can be done with a regular expression. These two statements are equivalent, but I've had better luck with the second one, so I tend to use it.

/^\/courses\/\d+\/settings$/.test(window.location.pathname);
/^\/courses\[0-9]+\/settings$/.test(window.location.pathname);

 

window.location.pathname returns just the path of current location, not the protocol, hostname, or any query parameters (anything after the ?).

 

Now we see that it only works on the right pages. I added a console.log() statement, which sends information to the browser's console. Here's the path and whether the statement is true or not. The bolding is mine.

 

  • /courses/896851/settings true
  • /accounts/97773/settings false
  • /courses/896851/pages/python-settings false
  • /courses/2151246/gradebook?show=settings false
  • /courses/896851/settings/configuration false

 

Notice this doesn't block it on the settings/configuration page, either. You would need to modify your regular expression to remove the $ if you wanted that. Now, technically this would trigger on pages like /courses/2151246/settings-are-beyond-our-control, but that page isn't going to be found in Canvas anyway, so your script will never run.

/^\/courses\[0-9]+\/settings/.test(window.location.pathname);

 

Limiting to TAs only

The next thing we need to do is make sure that it is only hidden for TAs and not for teachers.

 

That's checked by the following code.

ENV.current_user_roles.indexOf('ta') > -1

 

The roles listed in the ENV.current_user_roles variable are in an array. The indexOf() function in JavaScript tells you the position of an item in an array. The position is 0-based and returns -1 if the item isn't found. So if the indexOf() is greater than -1, it was found.

 Here's mine, but remember that I'm not a TA.

 

ENV.current_user_roles.indexOf('ta') > -1

In my list of roles, searching for "teacher" returns 2 (again, it's 0-based) and seaching for 'ta' returns -1 because I'm not a TA anywhere in Canvas.

 

If someone had a role of 'ta' listed in ENV.current_user_roles, then that check would be true.

 

Here's the ENV.current_user_roles for someone who is a TA.

 

Notice the lack of ta in that list. Now, at our school, we have very few TAs but we have some roles that look like they are TAs.

  • TA is the built-in role
  • Student TA is based off the student role
  • ACCOM Ta is based off the TA role

 

That image is from someone who was an accommodations TA, and it didn't contain the 'ta' user role. So then I went into Canvas Data and found a user who was just a regular TA. I got the same results, 'ta' was not listed. I then added myself as a TA to class that wasn't mine. Guess what the results were? Yep, my roles didn't change, the 'ta' doesn't appear in there.

 

That right there is enough to make that JavaScript always be false, which means that it won't work. Now there may be people doing something differently than we are where it does work, but nothing I can do will make 'ta' show up in the ENV.current_user_roles.

 

While I'm at it, I need to make a note about ENV.current_user_roles because it's often misunderstood. The name current_user_roles is ambiguous. People want to read it as "current - user roles" but it's really "current user - roles". In other words, it's roles that the person has anywhere in Canvas, not the roles that the person has in the current context (course in this case). This list does not change as you move around to different parts within Canvas. That means that if someone is a teacher anywhere in Canvas, this will always contain "teacher".

 

If 'ta' did show up in ENV.current_user_roles, it would be there everywhere. So if someone was a TA in one course and a teacher in the other, they would lose the ability to export the content even in the courses where they are the teacher. Worse yet, if someone was an admin somewhere and a TA somewhere else, they would lose the ability to export the content everywhere if 'ta' showed up in the list.

 

So how do I know if a person is a TA?

That, it turns out, is a really complicated question to answer. I'm giving a shout-out to Christopher Esbrandt and Danny Wahl on this because they are the two people who seem to know more about this than anyone else in the Community. Here are a couple of posts that refer to determining roles, but I think you'll have to tweak the code to meet your purposes.

 

Basically, knowing whether a person is a TA involves making a call to the the API to get a list of enrollments for the current user. AJAX calls are made asynchronously so the browser doesn't lock up while it waits for the information to be returned. That makes handling it slightly more challenging, but such is the way with JavaScript and browsers.

 

The lookup won't take long, normally, but if you want to make sure that it is only applying to TAs, then that's the approach to take. You can stick it inside the code that makes sure you're on the right page so that it doesn't do it to every page.

 

An alternative approach

 

Because the ENV.current_user_roles is global and available without needing another call to the API, many people needing to hide from selective roles end up blocking it for any role except for roles they trust.

 

In this case, you can probably trust teachers and admins to not do it, but you would want to block anyone else. This code will be true if the person is a teacher, admin, or root_admin. It will be false if they don't have any of those roles.

ENV.current_user_roles.indexOf('teacher') > -1 
|| ENV.current_user_roles.indexOf('admin') > -1
|| ENV.current_user_roles.indexOf('root_admin') > -1

 

Also note that's normally on one line, I just broke it up to make it easier to read.

 

Running on the right page with the right role

 

At this point, I would replace the first line that checks the page and role with this

if (/^\/courses\/[0-9]+\/settings$/.test(window.location.pathname) && (ENV.current_user_roles.indexOf('teacher') > -1 || ENV.current_user_roles.indexOf('admin') > -1 || ENV.current_user_roles.indexOf('root_admin') > -1)) {

 

If you prefer to see it broken up so it's easier to read, here it is.

if (/^\/courses\/[0-9]+\/settings$/.test(window.location.pathname) &&
    (
        ENV.current_user_roles.indexOf('teacher') > -1 ||
        ENV.current_user_roles.indexOf('admin') > -1 ||
        ENV.current_user_roles.indexOf('root_admin') > -1
    )
) {

 

Finding the Right Element

The goal is to hide the right element from the list. That means that we need to find a CSS selector that matches just that element and nothing else.

 

It's inside the div with an id of right-side-wrapper and it's also inside an aside that has an id of right-side. We can use those to make sure that it doesn't affect other parts of the page.

 

For some of those buttons, there is an id or a class added, like student_view_button or import_content.

If you were wanting to hide one of those, it would be much easier because the selectors would be easy to create.

 

For example, here are three ways to get rid of the Reset Course Content button.

$('aside#right-side a.reset_course_content_button').hide();
document.querySelector('aside#right-side a.reset_course_content_button').style.display='none';
document.querySelector('aside#right-side a.reset_course_content_button').remove();

The first one uses jQuery to hide it, the second does the same thing in pure JavaScript. It's still on the page, it's just hidden, so if someone went in and looked at the code, they could see it. 

 

The third one actually removes it from the DOM using the ChildNode.remove() method in JavaScript. When you do that, it's now gone completely until the page is reloaded.


The problem here is that the one we want to get rid of doesn't have any special classes or IDs. That means that we need to find a different CSS selector.

 

What the suggest script does is look for the download icon as the it's the only item in the list that has it.

The download icon is contained inside of an italics element within an anchor element. The class on that italics is icon-download.

 

Then, it grabs the parent(), which is the anchor element containing the hyperlink and removes it. 

$('.icon-download').parent().remove();

 

That works ... as long as the icons don't change. While Canvas is unlikely to use the same icon twice on that page, someone might add on a script that exports something else for the course and decide to use a download icon with that and this script would remove all occurrences of it.

 

Going back to getting the location correct, if you didn't get the check for the right page down and your Python Settings page included coded where someone added the Download image as part of the information on the page, it would also get removed.

 

You may have seen people using something like nth-child() in their selectors. My warning against this cannot be strong enough. Do NOT do this!!!

$('aside#right-side div a:nth-child(8)').remove();

The problem is that it is very sensitive to the position of the item in the list on a page that you don't have control. It may be the 8th item for someone, but it might be the 9th for someone else or the 3rd for another person, depending on the roles and permissions they have. If you do have control over the page, then add a class to make it uniquely identifiable.

 

There is another selector that we can use. It's called an attribute selector and allows you to match based on the contents of an attribute. These are contained inside brackets [ ] and there are modifiers you can use. You just need to figure out what to match.

 

Here's the full button code inside the DOM.

<a class="Button Button--link Button--link--has-divider" href="/courses/2151246/content_exports">
      <i class="icon-download"></i>
      Export Course Content
</a>

 

When you strip that down to the essentials, we get

<a href="/courses/2151246/content_exports"></a>

So we need to match an anchor element with href="/courses/2151246/content_exports".

 

There's an issue here. Your course doesn't have ID 2151246 and you want this to happen for all courses, not just one particular course. So we cannot compare off the full string, we need to find a part that matches.

 

Luckily, it's the /content_exports at the end that matters. And the attribute selectors have a way to look for something at the end of the attribute, it's $=

 

To specify this element, inside the aside, you could do use this selector

aside#right-side a[href$="/content_exports"]

That returns the hyperlink element, so you don't need to get the parent when you remove it or hide it. 

 

This code will remove it. You only need one of these lines, the top line is jQuery and the bottom line is pure JavaScript.

$('aside#right-side a[href$="/content_exports"]').remove();
document.querySelector('aside#right-side a[href$="/content_exports"]').remove();

You could also just hide it instead of removing it by using .hide() in jQuery or .style.display = 'none' in JavaScript.

 

jQuery or JavaScript?

There's a couple of times that I've mentioned code for both jQuery and JavaScript. The jQuery library is included with Canvas and they've guaranteed that it will be available to you before your page loads. JavaScript is built into the browser and so it's always there.

 

When I first started writing code to add to Canvas, I used jQuery. The more I learn, the more I use JavaScript. That's me, but jQuery does make things easier.

 

For example, in that code block in the last section, if the Export Course Content button isn't on the page, then the jQuery will not do anything while the JavaScript will throw an error and your code will stop working.

You can watch out for that by either using a try {} catch{} block or by checking to make sure that the document.querySelector() actually found something.

 

Another difference is that jQuery will hide or remove all items that match your selector, while document.querySelector will only match the first one. You'll need to use document.querySelectorAll and then that returns a NodeList which is kind of like an array in someways but not in other.

 

If you're confused right now and to remove all occurrences, which you probably do, you might just want to stick with the jQuery.

 

Putting it all together

 

So where are we at? Here's the final jQuery code wrapped so it's readable.

if (/^\/courses\/[0-9]+\/settings$/.test(window.location.pathname) &&
    (
        ENV.current_user_roles.indexOf('teacher') > -1 ||
        ENV.current_user_roles.indexOf('admin') > -1 ||
        ENV.current_user_roles.indexOf('root_admin') > -1
    )
) {
  $('aside#right-side a[href$="/content_exports"]').remove();
}

 

Here it is so that it's easier to copy/paste.

if (/^\/courses\/[0-9]+\/settings$/.test(window.location.pathname) && (ENV.current_user_roles.indexOf('teacher') > -1 || ENV.current_user_roles.indexOf('admin') > -1 || ENV.current_user_roles.indexOf('root_admin') > -1)) {
  $('aside#right-side a[href$="/content_exports"]').remove();
}

 

Here's the whole thing in pure JavaScript, checking to make sure that the button actually exists before you try to remove it. It also throws it inside a closure generated by an Immediately-Invoked Function Expression to protect the variables from polluting the global namespace.

(function() {
  'use strict';
  if (/^\/courses\/[0-9]+\/settings$/.test(window.location.pathname) &&
    (ENV.current_user_roles.indexOf('teacher') > -1 || ENV.current_user_roles.indexOf('admin') > -1 || ENV.current_user_roles.indexOf('root_admin') > -1)) {
    var el = document.querySelector('aside#right-side a[href$="/content_exports"]');
    if (el) {
      el.remove();
    }
  }
})();

 

You can probably see how jQuery makes it easier to do things.

 

And yes, I know there are alternative ways to do things. I could come up with a fancy way of determining whether any of the elements in one array were contained inside another array using the map() function, but if you're going for understanding the multiple if conditions is simpler.  Also realize this that is mostly mean to discourage behavior rather than actually prevent it.

Outcomes