Hiding Content from Certain Roles

James
Community Champion
30
8235

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  @kevinw  wanted to remove the "Export Course Content" link from the right sidebar, but only for TAs. dgrobani‌ 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.

 

238569_pastedImage_5.png

 

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.

238620_pastedImage_14.png

 

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)

238533_pastedImage_2.png238534_pastedImage_3.png

 

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

238601_pastedImage_4.png

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.

238602_pastedImage_5.png

 

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.

238603_pastedImage_6.png

 

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

 

238605_pastedImage_8.png

 

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.

238606_pastedImage_9.png

 

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

238607_pastedImage_10.png

 

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.

238608_pastedImage_21.png

 

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.

238610_pastedImage_23.png

 

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.

238611_pastedImage_25.png

 

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.

238612_pastedImage_26.png

  • 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 cesbrandt‌ and  @tdw  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

August 25, 2018, update.  @dtod  pointed out that there should be a negation in the check. If you want to allow the removal for anyone except the indicated roles, then you should preface it with a !

 

Because that is what we are trying to do here since we cannot select just TA's, I have modified the code to put the negation in there. If you want to limit the change to just certain roles that match, then you should not have the !

 

In either case, make sure you understand what is going on before you run the code.

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.

 

238616_pastedImage_33.png

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.

238570_2017-06-13_21-53-48.png

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.

238621_pastedImage_15.png

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

238622_pastedImage_16.png

 

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="https://community.canvaslms.com/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.

238623_pastedImage_19.png

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.

August 25, 2018, update. Thanks go to  @dtod , who pointed out that there should be a negation in the check if you want to exclude it from everyone except those in certain roles. If you want to exclude it from people matching certain roles, then do not include the ! in front of the role check.

30 Comments
MattHanes
Community Champion

This is an incredibly thorough and helpful explanation. Well done, sir! 

kona
Community Coach
Community Coach

Wow... just wow! I fully admit I only understood about 10% of all that, but I'm still amazed by how in depth you went on this and what a great resource this will serve for people. Great work!

PS When I shared that question with you I was only hoping you might share a few JavaScript resources, not spend the evening tearing into this so thoroughly. Thank you!

James
Community Champion
Author

The problem with JavaScript resources is that, for the basic stuff, you're not trying to learn JavaScript, you're trying to accomplish a specific task. There's no book or website that I want to sit through the whole thing to learn how to program in JavaScript. I don't want to start with "Hello world." I don't have time to start with "Hello world." I want to solve the problem that needs solved and get back to life.

My primary resources are the encyclopedia (Mozilla Developer Network documentation) and the commentary on it (Stack Overflow), using Google to get me to the content I need. I often start with a solution from Stack Overflow and then go look up the right way to do it using MDN.

tdw
Instructure
Instructure

There's nothing wrong with a learn-by-doing approach... well, I guess as long as you're getting your learning done in Test Smiley Happy

This is a great write up, and I really like your regex approach to looking at the path.  I usually shy away from regex because I'm never really certain that it's working the way I want it to.  I've approached it this way:

var path = window.location.pathname.split( '/' );
if( path[1] == "courses" && path[3] == "settings") {
  //do stuff
}

Then you can play around with other things like "path[4] == undefined" to make sure you're not at a sub-path, etc...  But I'm going to give your regex a try, I'm always for shorter, cleaner, code!

kevinw
Community Contributor

Well my head hurts a little but this was an amazing blog posts. I feel like hackerman now. 

James
Community Champion
Author

Agreed! I developed this blog in beta. Definitely don't do this in production!

I've long been a fan of regular expressions and try to use them when possible, probably because I'm used to doing a lot of free-form text rather than structured paths.

You also get the ability to make sure a number in-between, which makes the splitting harder. I normally create a new RegExp() first, but I was going for simplicity here. If you create a new regular expression object, you don't have to escape the forward slashes, so it's easier to read.

I also use a capturing regex with the exec command. For example, this can be used to make sure you have a valid route and get the course ID from the route.

var validPath = new RegExp('^/courses/([0-9]+)/settings$');
var matches = validPath.exec(window.location.pathname);
if (matches) {
  var courseId = matches[1];
}

 

I know the ENV.COURSE_ID may contain the course ID, but I also don't have access to the official docs to know if it's always available within a course or that it will always be maintained, so I started grabbing IDs from paths. 

Another place I use exec is when I'm trying to scrape the page for information. Like when the page says "19/20 points" or something similar, using a regular expression there makes it easier to find than having to split on spaces and then split on slashes.

Note that the test() method returns a Boolean while exec() returns an array or null if there are none. So you have to pay careful attention to things when using Regular Expressions. If all you want to do is see if the path matches, you can do

var validPath = new RegExp('^/courses/[0-9]+/settings$');
if (validPath.test(window.location.pathname)) {
// do something
}‍‍‍‍‍

There are also comments all over the place about overhead incurred by using Regular Expressions when a string method would work as well.

But I think your comment also shows how difficult it would be to write a single guide that explained JavaScript to someone wanting to do something "small" like remove a button. They would never make it through all the minutia to get to the point of removing the button.

For example, you've got Danny, who is trained in JavaScript and has taught other people to program in it; and you've got me who writes stuff, but isn't formally trained and my code is influenced by other languages. That can be confusing to people who think  in my first example that "var courseId" is visible only within the if{} block because they've been programming in PERL and don't realize that JavaScript hoists all the variables to the top. That's also why I recommend putting all of your code in a closure so it doesn't pollute the global namespace accidentally and applying 'use strict'; when you write code. I see a lot of people using $(document).ready(function{});  because that's the way it's always been done, even though jQuery documentation doesn't recommend it, other sites say it's not necessary, and the way that Canvas loads jQuery means the document is ready before we get to touch it.

So yes, if you want to do something major -- learn the language or enough of it to know what you're doing to the rest of the system (that's mainly why I don't use setInterval). But often you can just take what someone has done and tweak it to meet your needs.

tdw
Instructure
Instructure

Ha, you give me too much credit, that should read "Danny, who has hacked through JavaScript for a while, and has taught other people" :smileysilly:

Also, I just remembered the site regex101.  It does all kinds of highlighting and stuff AND generates unique URLs that you can share to continue to revise.  For example, here's one I did a while back to isolate quizzes in a path:

Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript 

I kinda like that as an alternative to the boolean evaluations for development - but throwing it in a boolean is a great idea for production!

thompsli
Community Champion

Thank you for writing this! I'm trying to get up to speed in how to optimize Canvas for my own needs, and it really helps me to see the thought processes and problem-solving steps like this. I have no need of this particular functionality, but this helped me get a better grasp on where to start with my own JavaScript-based solutions since it stepped through how to start with an existing page and figure out what to add where, which means I can generalize a lot of this to the things I actually need to figure out.

(My coding background is basically in C as taught by a liberal arts school that thought "industry" was a dirty word, so while I can write psuedocode to do all kinds of exciting things to red black trees if I put my mind to it, getting a window into how to dig into an existing thing and make practical changes is very helpful when I'm actually trying to write code to solve a non-CS-based problem rather than write code as an end to itself.)

James
Community Champion
Author

Thanks. That's what I was hoping for when I wrote it -- to show what you need to do, rather than just doing it for you.

Chris_Hofer
Community Coach
Community Coach

Hi  @James ‌...

We have a "Master Term" in Canvas where we house a "master" version of every course we build.  In these "master" courses, we typically enroll one or two people with the "Teacher" role (so that they can edit content as need be) and all other people with a custom "Viewer" role which is based off of the "Teacher" base type (so that these people can view, not modify, the content of the course...yet still import content from the "Master Term" to their own Fall/Spring/Summer course.  Recently, we were made aware of an issue where someone with our custom "Viewer" role in a "master" course clicked on the "Reset Course Content" button in "Settings" which wiped out the "master" course content, and we had to go back and recover it.  I read through your blog (though not completely understanding much of it).  Would it be possible, via the global JavaScript file, to hide the "Reset Course Content" button from our custom "Viewer" role?  We'd like to try and prevent situations like this from happening again.

James
Community Champion
Author

Sounds like you would need to incorporate some information about getting the user roles and not just the account roles. I didn't touch on that here other than to mention it and refer to some other places in the Community where others have written about it. Since then, I provided some broad directions myself: https://community.canvaslms.com/thread/17731-user-roles-in-javascript 

You could use that with the viewer role, but if you only wanted it in the viewer role within certain terms, then you would need to add logic to check the term as well.

mzimmerman
Community Coach
Community Coach

Thanks for posting this!  It seems like a really helpful starting point for I task I've been working on, which is hiding the "Add External App" button from teachers in the course settings area.  (We want them to be able to pick from vetted/whitelisted apps in the App Center, but not add their own apps...)

Unfortunately, I'm having some difficulty with the "Finding the right element" portion of the task!  I can easily locate it in the source, but I can't figure out how to get the code to recognize it.

Of course, I'm also new to JavaScript, so I'm pretty much just trying to hack up other people's code to find something that works!

Is there any go way to make use of the "Copy Selector Path" or "Copy Xpath" tools in the browser "Inspect Element" screen to help JavaScript pick out the thing I want to hide?

Thanks!

Chris_Hofer
Community Coach
Community Coach

 @mzimmerman ...

Are you signing in to the https://www.eduappcenter.com/  website to configure your whitelist so that only apps you want your instructor to see can be installed?

MattHanes
Community Champion

Hey  @mzimmerman , you can actually white list apps through a token generated on the EduAppCenter site. That's how we vet our apps that teachers can install: https://community.canvaslms.com/docs/DOC-12672-4214526787 

MattHanes
Community Champion

You beat me to it,  @Chris_Hofer !

mzimmerman
Community Coach
Community Coach

 @Chris_Hofer ‌,  @MattHanes ‌ - We have things whitelisted under Edu App Center, but I guess we were assuming that teachers still needed to have "LTI Add and Edit" permissions to use those.

Up to this point we have also allowed instructors to add their own LTIs, but are coming to the conclusion that we really need to lock that down in the interest of security, and are trying to figure out the best ways to do that while still allowing faculty some flexibility.

But if I can turn off the "LTI Add and Edit" permissions and still let teachers pick from the Edu App Center, that kinda makes hiding the +App button a moot point.. 🙂

Thanks, gentlemen!

dtod
Community Contributor

Isn't the final code missing a "not"?

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
)
)
James
Community Champion
Author

 @dtod ,

Good question -- and good eye.

I guess it depends on what you're trying to do. If you're trying to remove it for people having a specific role or roles, then no. If you're trying to remove it from everyone except for trusted roles, then yes.

In this particular case -- hiding the button from TA's, it should be in there. That was the impetus that started this. But I've also seen people who want to hide it from teachers, but not admins, so they may want to leave it in the positive.

I'll go through and edit the blog to try and clarify. Thanks for catching this.

robotcars
Community Champion

Last night, I was looking for a way to replace hasAnyRole() in our code, which I've been using since forever...

via instructurecon code · GitHub 

 

Read through the cesbrandt thread  @James  mentions above and liked the use of includes(). Ultimately, wanting to test for an array of 'allowed' roles against the users roles and came up with the following. Thought this was a good place to share and open for discussion.

 

['teacher','admin','root_admin'].some(a => ENV.current_user_roles.includes(a))
// expected output: true

// providing that
ENV.current_user_roles
// (6) ["user", "student", "teacher", "observer", "admin", "root_admin"]‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

 

Array.prototype.includes() - JavaScript | MDN 

Array.prototype.some() - JavaScript | MDN 

James
Community Champion
Author

It didn't take me nearly as long to follow the some() includes() as it did the code you've been using forever -- even though I had never encountered them before. .some() uses short-circuit Boolean evaluation as well so it doesn't waste looking after the first match.

MariaMatheas2
Community Explorer

Hi @James , 

I'm very new to Canvas, and apologies if this request is not actually related to this thread here, but I too am looking to hide content, not from TAs, but from students, and not buttons, but text on a page. What we are trying to do is have instructions on a page (which is part of a template) which a teacher/instructor can see in edit view, and which will guide/help them to fill that page with the correct information etc., but which will not be visible in student view (and which will also not be read by screen readers). I was hoping you might have some suggestions as to how this could be done?

Thank you!!

Maria 😊

James
Community Champion
Author

@MariaMatheas2 

Let me start with the disclaimer that I have not used a templating system within Canvas. I use templates on the back end to take data and create the Canvas page. The page that comes into Canvas does not have any templating information other than wrapping content in div elements that have a classname or ID to identify what content goes in there. When I update the template, it fetches the current version, looks for the appropriate content to change and leaves the rest alone. This is for me alone, not for others, and I understand what can and cannot be changed on the page. We're also at a school where the faculty create their own pages, there is no templating system in place, and we don't have TAs. My recommendations may not be suitable for you.

Given my frame of reference, there are two routes I would consider if I was using a templating system for others. The first is to place simple instructions within the template and have whoever is editing them remove them. If the documentation process is so complicated that it requires substantial instructions, then I would make the documentation a completely separate process from Canvas and not try to incorporate it into the content of the page.

The first route is relatively straight forward and if people cannot manage it, then you need to incorporate training or not allow them to make the changes themselves.

I'll focus on why I would not try to do this from within Canvas.

There's no element within the page that would indicate the role the person looking at it has. That information is sort of available through an environment variable (at least in the browser, I don't develop for the mobile apps), which is what previous posts have been about using. That means that there is no simple solution where you can use a CSS selector like body.student .template_instructions to selectively hide the information.

Without using custom JavaScript at the (sub)account level, you're not going to be able to hide the information.

Hiding is probably the wrong approach anyway. If you put the information in the page itself, then students will be downloading it and determined students with a little technical skill will be able to view it through the browser's source view. That forces them to download extra information that they don't need. The user content for pages is actually delivered inside a script element and then processed at some point using Canvas' JavaScript and rendered on the page. This means that you will have to wait for the content to be ready to manipulate before you can remove it. Depending on how you process it, that template information may be noticeably be visible and then disappear to the user, which isn't the best for accessibility.

A better different approach might be to insert the information by using custom JavaScript when the page is edited. Then your JavaScript could look at the location and make sure that has /edit at the end to distinguish from viewing it as opposed to editing it. Your template could use a classname or aria-data attribute that your JavaScript could look for and insert the code.

That has its own set of problems, though. The biggest would be removing the information from the page before saving it. You could take over the save and save and publish buttons so that they fired your events to clean up the page and then propagated to the event listeners that Canvas had on them to start with.

Canvas doesn't allow you to lock certain parts of the Rich Content Editor or have blocks that are editable while others are not. That means that whoever edits the page could mess up the template in ways that your code wouldn't be able to find what they have done. That's not just a problem for removing the template before saving, that's would be a problem for hiding the content as well.

If you don't want to check the page before saving it, then one way around that would be to set up a live event to look for a wiki_page_updated event and let that trigger a script on a server that loads the page, cleans it up, and gets it ready for delivery to the students. Those changes can be detected relatively quickly, but not immediately, so there is a potential that students would see a broken template for a short period of time.

If I was forced to make the instructions be online, I would put in custom JavaScript that looked for locations ending in /edit so I knew they were people actually editing the page. Then I would add a button in a prominent position that linked to instructions. Then the person would see that instructions are available, but they would be separate from the content and I wouldn't have to worry about hiding it from students or removing it from the content after someone edits the page.

MariaMatheas2
Community Explorer

Thank you so much for this helpful and thorough response @James A lot to think about! I'm a fan of your suggestion of placing simple instructions within the page which academics would then have to delete, sort of like placeholder text, and combining that with training if necessary. 

I was wondering whether you think it might be possible to find a way to make instructions appear only in the editor view. So nothing to do with roles, the text would be hidden from teachers as well, unless they open up the editor. I don't mean the raw html editor, but the regular, WYSIWYG editor. I'm asking because apparently at a university here in Melbourne they found a way to do it, not in Canvas of course!, but in the D2L. I had a look at the html code associated with it, but there doesn't appear to be anything in there to suggest how they managed it. There is a data-ally-exclude attribute in each of the <p> tags, and I got excited at first thinking that might be it, but apparently that just tells assistive technology (through the ally plugin) to avoid reading our the text, I think. It's so strange, nothing to suggest that it be made invisible when rendered. 

Thanks again!

Maria

James
Community Champion
Author

@MariaMatheas2 

The solution to get text to appear only in editor view is what I was talking about with the looking at the window.location.pathname and matching seeing if it ended in /edit.

Modifying the element but only for those editing the page was what my whole section on making sure you strip it out afterwards was about. That is low on my list of things to attempt, though. The Rich Content Editor is TinyMCE and it is loaded in an iframe. Depending on how the iframe is loaded, they may be little that you can do inside of it. Christopher Esbrandt got an html source editor in there (Canvas recently came out with their own), so I'm not saying it's impossible to manipulate it, just difficult.

One thing you might do to keep people from messing up the template is to use the contenteditable attribute on items you don't want them to change. Setting contenteditable=true will allow them to change the content (the default) and setting contenteditable=false will prevent them from content.

There are problems with this approach that need considered. contenteditable is not in the Canvas HTML Editor Allowlist, which means that it cannot be saved in part of the document itself and would have to be inserted through JavaScript. If it were allowed, then your task would be a lot easier. A second problem is that clicking the "HTML view" button and coming back will strip out the contenteditable attribute and you've lost all of your settings. That means that you need to put MutationObservers in there to watch what happens.

Depending on how tightly you need to control things, another option might be to use an external form to prepare the data. You could direct them to a page that provides and instructions and has multiple textboxes where they enter the information that goes into each section. On your server, you then take all of the data they submit and prepare the page for them and save it to Canvas. If they need to link to content within Canvas, then that may become problematic.

MariaMatheas2
Community Explorer

Thank you so much @James and I'm sorry! The javascript stuff is completely beyond me, to be honest, that's why most of your first response probably went over my head. Apologies for that! I will pass on your suggestions to a colleague and I'll let you all know how we get on with it. Thank again for all your help!

James
Community Champion
Author

@MariaMatheas2 

What you're wanting to do isn't trivial and requires programming skills. There are some templating systems available and you might want to search for an existing solution rather than trying to develop your own. Those template systems will likely require you to install JavaScript, but that is a lot easier than writing code from scratch.

MariaMatheas2
Community Explorer

Thank you @James 🙂

saidayouris
Community Member

That's great sir BRAVO, but for example
I add new role depend on "designer" or "TA" role called "Driver" for example,
and I add an external tool to global navigator , this external tool it's visible just for admins not for teachers and students
but I need to make it visible for this new role 'Driver'

This is my code

(function() {  
    'use strict';  
    if (!(ENV.current_user_roles.indexOf('admin') > -1 || ENV.current_user_roles.indexOf('root_admin') > -1 || ENV.current_user_roles.indexOf('ta') > -1)) {  
          var el = document.querySelector('.globalNavExternalTool');
              if (el) {
                    el.remove();    
                    }  
                }
            }
)();


I try with "ta" and "designer" roles but the external tools removed,
I know because when I connect with this Driver and typed in console
ENV
he gives me :

  1. current_user_roles: Array(2)
    1. 0: "user"
    2. 1: "teacher"
    3. length: 2

but I'm not a teacher I'm Driver based of TA role or Designer role !
Can you help me with this please ?
and thanks

James
Community Champion
Author

@saidayouris 

The environment variable will never contain Driver. It contains the Canvas-defined roles, not any custom roles. TA is based off TeacherRole, so if Driver is based upon TA and TA is based upon Teacher, that might be why you're seeing Teacher.

The current_user_roles is not context specific. It does not change when you are inside a course. So if you are a teacher anywhere in Canvas, you will have that user role in the environment variable everywhere in Canvas.

If you need custom roles within a course context, then you will need to fetch the roles for that context, most likely using the Enrollments API for the course. See the links in the "So how do I know if a person is a TA?" section of the blog for more information.

saidayouris
Community Member

@James
thank you for your explanation