cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
jsimon3
Community Participant

Missing Assignments Observer

New endpoint humans so let's get cooking. 

GET /api/v1/users/:user_id/missing_submissions

 

This endpoint is what I have been needing for some time in K-12. In fact I have had to bake it myself with a now retired c# LTI application that provided the parents with the same information.

 

proof of concept:

https://youtu.be/K-1KWvnWXAY

 

. Enough chat here is the code:

 

 

 

function missingSubmissionsObserver() {
  const checkIfNull = async selector => {
    while (document.querySelector(selector) === null) {
      await new Promise(resolve => requestAnimationFrame(resolve));
    }
    return document.querySelector(selector);
  };
  checkIfNull(".recent-activity-header").then(() => {
    if (ENV.current_user_roles.includes("observer")) {

      const observeePage = document.querySelector(".recent-activity-header");
      const fetchObservees = `/api/v1/users/self/observees?per_page=30`;
      const options = {
        credentials: "same-origin",
        headers: { accept: "application/json" },
        timeout: 5000
      };
      let observees;
      let dataCollection = [];
      let missingSubmissionsContainer;
      let observeesListContainer;
      const borderStyle =`border-bottom: 1px solid #005299;padding-left:8px;`;
      const tableClasses = [`ic-Table`, `ic-Table--condensed`];
      const filteringConfig = {
        'l8 sub after': `2020-08-01T01:01:01Z`, //ISO or function
        'locked for a user': false, //bool
        'omit from final grade': false, //bool
        'display points possible if greater or equal to': 0, //num
        'submittable':`&filter[]=submittable` //either `&filter[]=submittable` || ``
      };
      //------------------------------------------------------------------
      const 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;
        }
      };
      //--------------------------------------------------------------------
      const fetchObserveesID = async () => {
        const response = await fetch(fetchObservees, options);
        const info = await response.json();
        let rsp = {
          data: info,
          ok: response.ok,
          headers: response.headers
        };
        observees = rsp.data;
      };

      fetchObserveesID().then(async () => {
        observeePage.insertAdjacentHTML( 'beforebegin',
        `
        <h2 id="being-observed">Students Being Observed</h2>
        <a href="/grades" class="Button">View Grades</a>
        <div class="gameLoader pacMan"></div>
        <br>
        <h5>Click a name below to see missing submissions</h5>
        <div class="observees-list-container"></div>
        <div class="missing-submissions-container"></div>
        `
        )
        ;
        observeesListContainer = document.querySelector(
          ".observees-list-container"
        );
        missingSubmissionsContainer = document.querySelector(
          ".missing-submissions-container"
        );

        //LOOP
        observees.forEach(kid => {
          let missingListItem = document.createElement("li"); //create li
          let classesToAddOB = [`Button`]; //add more classes
          missingListItem.classList.add(...classesToAddOB);
          missingListItem.textContent = `${kid.name}`; //replace
          missingListItem.addEventListener(
            "click",
            async () =>
              await addMissingSub(kid.id).then(async () => {
                scrubMissingSubs(kid.name);
              }),
            { once: true }
          );
          observeesListContainer.appendChild(missingListItem);
        });
      });
missingSubmissionsObserver()

 

 

NOTES:

The custom loading animation the pacMan, but you can easily replace with the canvas native loader. 

 

 

/*
 _ __   __ _  ___ _ __ ___   __ _ _ __  
| '_ \ / _` |/ __| '_ ` _ \ / _` | '_ \ 
| |_) | (_| | (__| | | | | | (_| | | | |
| .__/ \__,_|\___|_| |_| |_|\__,_|_| |_|
| |                                     
|_|                                     
*/
.gameLoader {
  width: 15px;
  height: 15px;
  position: relative;
  display: inline-block;
  margin-left: 7px;
}
.gameLoader::before,
.gameLoader::after {
  content: "";
  position: absolute;
}
.pacMan {
  border-radius: 50%;
  width: 4px;
  height: 4px;
  -webkit-animation-name: pacmanDot;
  animation-name: pacmanDot;
  -webkit-transform: translateX(14px);
  transform: translateX(14px);
}
.pacMan::before,
.pacMan::after {
  border-radius: 50%;
  border: 14px solid #005299;
  border-right-color: transparent;
  top: -12px;
  left: -24px;
}
.pacMan,
.pacMan::before,
.pacMan::after {
  -webkit-animation-duration: 0.5s;
  animation-duration: 0.5s;
  -webkit-animation-timing-function: linear;
  animation-timing-function: linear;
  -webkit-animation-iteration-count: infinite;
  animation-iteration-count: infinite;
  animation-play-state: var(--playState, paused);
}
.pacMan::before {
  -webkit-animation-name: upperJaw;
  animation-name: upperJaw;
}
.pacMan::after {
  -webkit-animation-name: lowerJaw;
  animation-name: lowerJaw;
}
@-webkit-keyframes pacmanDot {
  0%,
  50% {
    background: #008000;
  }
  51%,
  100% {
    background: none;
  }
  0%,
  100% {
    -webkit-transform: translateX(19px);
    transform: translateX(19px);
  }
  50% {
    -webkit-transform: translateX(12px);
    transform: translateX(12px);
  }
}
@keyframes pacmanDot {
  0%,
  50% {
    background: #008000;
  }
  51%,
  100% {
    background: none;
  }
  0%,
  100% {
    -webkit-transform: translateX(19px);
    transform: translateX(19px);
  }
  50% {
    -webkit-transform: translateX(12px);
    transform: translateX(12px);
  }
}
@-webkit-keyframes upperJaw {
  50% {
    -webkit-transform: rotate(50deg) translate(2px, -2px);
    transform: rotate(50deg) translate(2px, -2px);
  }
}
@keyframes upperJaw {
  50% {
    -webkit-transform: rotate(50deg) translate(2px, -2px);
    transform: rotate(50deg) translate(2px, -2px);
  }
}
@-webkit-keyframes lowerJaw {
  50% {
    -webkit-transform: rotate(-50deg) translate(2px, 2px);
    transform: rotate(-50deg) translate(2px, 2px);
  }
}
@keyframes lowerJaw {
  50% {
    -webkit-transform: rotate(-50deg) translate(2px, 2px);
    transform: rotate(-50deg) translate(2px, 2px);
  }
}

 

 

-  there is a bit more filtering that is available for the grades displayed it is commented out under the ``` dataCollection ``` filter 

-  I don't like my appendChild process but it is easier to read that way vs. me extraping and throwing into convoluted functions so I have to force myself to get use to seeing such an eyesore (easier to troubleshoot incase I abandoned ship). But I probably will anyway it hurts my soul...

 

Edit: 2020-02-15T15:58:38Z implemented proposed changes  @r_carroll ‌ the var is filteringConfig

2 Replies
robotcars
Community Champion

 @jsimon3 ,

This one is really cool, we are considering adding to our themes, so thanks for sharing.

We had some requests and thoughts

Some kind of note that if no missing assignments show up, that it may be because due dates aren't set for assignments.

I'm also curious to know if there's a way to mark rows where an assignment is 'submittable' and show all missing assignments, not just default it to filter by submittable. Or maybe pull both and sort submitted at the top?

Can you explain the other filtering that's happening in dataCollection? Could the filtering be a few checkboxes that allow the user to controll?

Does it make sense to use course.start_at instead of the fixed date on L#149?

I need to change the loader, I have one from our custom CSS, but was wondering what you'd change to replace it with the default Canvas loader?

I pushed a minified version to our CDN, and loaded it for observers only.

// adds custom options for observers
(function () {
'use strict';

if (['observer'].some(a => ENV.current_user_roles.includes(a))) {
$.ajax({
url: '.../branding/js/observer-missing-assignments.min.js',
dataType: 'script',
cache: true
})
}
})();‍‍‍‍‍‍‍‍‍‍‍‍
jsimon3
Community Participant

 - by note do you mean? if missing assignments length is 0 write: "if no missing assignments it may be because due dates aren't set for assignments."

-The filters reside here:(I will extrap and place filter vars at top of script for easy access)

let filteredDataCollection = dataCollection.filter(
          lex =>
            lex.due_at > "2020-01-01T01:01:01Z" &&
            !lex.locked_for_user /**&&
            !lex.omit_from_final_grade &&
            lex.points_possible >= 1 **/

        );

The current filters that I have listed for this example are(via the assignments object):

  the due date for the assignment.

         due _at -> where you can specify a Date() (I actually have this pulling from a terms script that I wrote so what          I will do for this is just create a config var for it and place at top of script)

  Whether or not this is locked for the user.

         locked_for_user -> true or false

  If true, the assignment will be omitted from the student's final grade

         omit_from_final_grade true or false

  points possible not zero (sometimes teachers will mark zero grade assignment so things show up on the    calendar terrible practice but some still choose to practice that way )

      points_possible >== 1

- submittable is a partition of the api call I have that set to default for everyone

      /api/v1/users/${progeny}/missing_submissions?include[]=course&filter[]=submittable&per_page=100`

           I could just turn that into a var and push with the configs as well like:

      /api/v1/users/${progeny}/missing_submissions?include[]=course${config.submittable}&per_page=100`

 

- The filtering could be a few checkboxes that allow the user to control prior to the script running like a helper that sets    the parameters for the script?

 

-I have some witchcraft and prayers running on my side that does stuff for terms so I just placed a fixed date there on    #144 so anyone that wanted to use could... But even so if a teacher has a full year course and at semester the grading period is locked(SIS locked not Canvas assignment locked) would you want to show assignment prior to that locked time? But course.start_at wouldn't work for us...

 

-I did nothing to the default loader I just never called it...

 

On this:

// adds custom options for observers
(function () {
  'use strict';

  if (['observer'].some(a => ENV.current_user_roles.includes(a))) {
    $.ajax({
      url: '.../branding/js/observer-missing-assignments.min.js',
      dataType: 'script',
      cache: true
    })
  }
})();

Kool beanz in this case remove the redundant  

if (ENV.current_user_roles.includes("observer")) {

I will setup config and post by Monday