cancel
Showing results for 
Search instead for 
Did you mean: 
jsimon3
Adventurer

Add observers to email

Now this is not my idea... I found a git on gitHub that did exactly what I needed it to do the problem was that it was beefy(12k lines)(GitHub - sdjrice/msgobs: A JavaScript modification for the Canvas learning management system which a... ). So I forked it and was going to go to work on it, convert to plain JS modify, when I realized I had no idea WTH is going on. Seriously though, @sdjrice was on another level. So I scraped the project and decided to go in from scratch. I created a baby version of that. So it has a ton of stipulations.

One stipulation that will not disappear is that a course must be selected in order to use.  

  1. can only be used once per session--- this is a stipulation that I imposed because we have very click happy people. You can remove by switching the bool from {once: true} to false 
  2.  This does not work on the grade book.-I am no longer concerned w/this deemed not that important by staff
  3. If you were to decide to remove one of the students from the email there is no direct line to retract the parents from the email. I am no longer working on this as well.

337240_pastedImage_4.png

337238_pastedImage_2.png

I tried to embed the proof of concept no luck so here is the link:

https://www.iorad.com/player/1618642/Add-observers-to-email

Now anywhere you might see a set of // // those belong to a custom CSS animation I can share it if anyone is interested its the pacMan that you see nest to the button. Or you can use the integrated loading animation in Canvas.

function addObserverEmailButton() {
const checkIfNull = async selector => {
while (document.querySelector(selector) === null) {
await new Promise(resolve => requestAnimationFrame(resolve));
}
return document.querySelector(selector);
};
checkIfNull("#compose-btn").then(() => {
if (
ENV.current_user_roles.includes("teacher") ||
ENV.current_user_roles.includes("admin")
) {
const delay = ms => new Promise(res => setTimeout(res, ms));
let conversationsNav = document.querySelector(
"div.ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix"
);
let parentButton = document.createElement("div");
let classesToAdd = [
"ui-button",
"ui-widget",
"ui-state-default",
"ui-corner-all",
"ui-button-text-only",
"includeObserver"
];
parentButton.classList.add(...classesToAdd);
parentButton.setAttribute(
"style",
"margin:0 2px; min-width: 110px; background-color:wheat;"
);
parentButton.innerHTML = "Include Observers";
conversationsNav.insertBefore(
parentButton,
conversationsNav.childNodes[1]
);

// //let pmLoader = document.createElement("div");
// //let classesToAddPM = ["gameLoader", "pacMan"];
// //pmLoader.classList.add(...classesToAddPM);
// //conversationsNav.insertBefore(pmLoader, conversationsNav.childNodes[2]);

conversationsNav.querySelector(".includeObserver").addEventListener(
"click",
() => {
let course = document.querySelector(
'.message-header-input > input[type="hidden"]'
);
let courseNum = course.value.split("_")[1];
let parentCollection = { data: [] };
let kiddosArr = [];
let emailParents = [];

if (course.value) {
//loading animation
// //document.querySelector(".gameLoader.pacMan").style.setProperty("--playState", "play");
//loading animation
const fetchObservees = `/api/v1/courses/${courseNum}/enrollments?type[]=ObserverEnrollment&per_page=100`;

function nextURL(linkTxt) {
if (linkTxt) {
let links = linkTxt.split(",");
let nextRegEx = new RegExp('^<(.*)>; rel="next"$');

for (let i = 0; i < links.length; i++) {
let matches = nextRegEx.exec(links[i]);
if (matches && matches[1]) {
//return right away
//console.log(matches[1]);
return matches[1];
}
}
} else {
return false;
}
}
//Next define your main call:

const OPTIONS = {
credentials: "same-origin",
headers: {
accept: "application/json"
},
timeout: 5000
};
async function main() {
const response = await fetch(fetchObservees, OPTIONS);
let data = await response.json();
let res = {
data: data,
ok: response.ok,
headers: response.headers
};
parentCollection.data = res.data;
await loop(res);
}
//And then your loop

async function loop(parents) {
if (nextURL(parents.headers.get("Link")) === undefined) {
return;
//otherwise keep going.
} else {
const RESPONSE = await fetch(
nextURL(parents.headers.get("Link")),
OPTIONS
);
let data = await RESPONSE.json();
let res = {
data: data,
ok: RESPONSE.ok,
headers: RESPONSE.headers
};
if (res.ok) {
//console.log(res.data);
parentCollection.data = parentCollection.data.concat(
res.data
);
//console.log(res.data);
//console.info(Object.keys(parents.data).length);
//console.info(Object.keys(parentCollection.data).length);
}
await loop(res);
//await loop(res);
//if you want to wait for it.

//You need to call it from within an async function.
}
}
//Now all you need to do is call the main function:
const waitForMom = async () => {
await main();
};
//or:
//await main();
//if you want to wait for it.
//You need to call it from within an async function !impotanto.
waitForMom()
.then(async () => {
//console.log(parentCollection.data);
document
.querySelectorAll(
'div.message-header-input input[name="recipients[]"]'
)
.forEach(kiddo => {
kiddosArr.push(kiddo.value);
});
//console.log(kiddosArr);
})
.then(async () => {
kiddosArr = kiddosArr.map(x => {
return parseInt(x, 10);
});
})
.then(async () => {
parentCollection.data.forEach(element => {
//console.log(element.associated_user_id);
if (kiddosArr.includes(element.associated_user_id)) {
emailParents.push([element.user_id, element.user.name]);
}
});
//}
})
.then(async () => {
await delay(1000);
if (emailParents.length > 0) {
//loading animation
// //document.querySelector(".gameLoader.pacMan").style.setProperty("--playState", "paused");
//loading animation
emailParents.forEach(parent => {
console.table(parent);
document.querySelectorAll(
".ac-token-list"
)[1].innerHTML += `<li class="ac-token" style="background-color:wheat;">
${parent[1]}
<a href="#" class="ac-token-remove-btn">
<i class="icon-x icon-messageRecipient--cancel"></i>
<span class="screenreader-only">
Remove recipient ${parent[1]}
</span>
</a>
<input type="hidden" name="recipients[]" value="${parent[0]}">
</li>`;
});
} else {
popUpError(
`
( ⊙0⊙) - I am not seeing an observers associated. Sorry. - (⊙▂⊙ )`
);
}
});
} else {
popUpError(
`
(×_×;-In order to add parents you must select a course first. Re-fresh to try again. -(o。o;)`
);
}
},
{ once: true }
);
}
});
//---------------------------------------------------------------------------
const popUpError = msgTxt => {
const msgHolder = document.querySelector("#flash_message_holder");
const timeout = 9000;
const daMsg = (msgHolder.innerHTML = `<div role="alert" class="ic-flash-static ic-flash-error popUp">
<div class="ic-flash__icon" aria-hidden="true">
<i class="icon-warning"></i>
</div>
<h1>${msgTxt}</h1>
</div>`);
daMsg;
setTimeout(() => {
let popUp = document.querySelector(".popUp");
popUp.parentNode.removeChild(popUp);
}, timeout);
};
//---------------------------------------------------------------------------
}
addObserverEmailButton();

Edit: 17-12-19: modified script to pull in a 100 parents if there, as well as made a few more async calls for better error handling and lengthy forEach if >50 parents(merged classes & lectures)

Edit: 10-02-20: modified script to pull in all paginated pages if there is pagination now working in large courses like grade counselors w/400+ students and 1000+ parents

5 Replies
robotcars
Navigator

Very nice. I really want to get into some observer hacks soon, but I need time to focus and do a bit of mashing up of other's ideas. I always had a problem with that hack too, boss wanted it, I said it's more lines than every other hack we have combined and I won't support it. Sometimes we just have to weigh the benefits and support.

Are you using IIFE's to wrap each of your hacks to keep them out of the global scope?

I actually use a combination of promises and mutation observers. It might not be the best way to do it but it gets the job done. 

const checkIfNull = async selector => {
    while (document.querySelector(selector) === null) {
      await new Promise(resolve => requestAnimationFrame(resolve));
    }
    return document.querySelector(selector);
  };
  checkIfNull("#compose-btn").then(() => {

So I have a ton of functions that are sitting in limbo waiting on their promises(in this case the #compose-btn which is only found in the compose message canvas function). So, every one of my "hacks" waits for a URL recognization and then it waits again for a particular element to be present to be present and then the actual function runs. SO.... this "hack" on my end is sitting in two more asynchronous functions. I think I might have answered the question IDK...

Not quite. The IIFE, protects the code from the global scope, ensuring that none of your declarations and Canvas declarations are interfering with each other. It's pretty benign, but extremely recommended.

ie. what if Canvas is using const checkIfNull?

Ben Alman » Immediately-Invoked Function Expression (IIFE) 

URL scoping with REGEX is a good idea, but I'm not seeing it in your example. I guess I'd recommend sharing that with your example code so that people don't have to try adding it into your example when they want to test it, with the added benefit of immediately knowing where in Canvas I need to go to test it.

Here's a short example of both. ccsd-canvas/course-add-people.js at master · robert-carroll/ccsd-canvas · GitHub 

  • IIFE protects this hack from everything else, I can even reuse my own variables between hacks, which makes development a lot easier because I'm not constantly having to name things uniquely.
    • Locked down to the course/users (people) page
      • start observer when modal button is clicked

Ohh that is what you mean... So I don't use var at all in my code I only use let or const, so all of my variables are blocked scoped. I actually use const checkIfNull   in all of my functions without the leakage of var. So it is not something I think about anymore. I thought the question dealt with invocation and performance.

jsimon3
Adventurer

Updated for pagination and loading animation. --Also put together new script for all new endpoints.