gabeabrams / inclass Goto Github PK
View Code? Open in Web Editor NEWAn edtech app store for Canvas, created by and designed for education-focused faculty and staff
An edtech app store for Canvas, created by and designed for education-focused faculty and staff
This update to server/Store
will add reading of each app's install.xml
and credentials.json
file to the loading process.
Add the contents of install.xml
to a new field in the app: app.installXML
.
Add the contents of credentials.json
to a new field in the app: app.installationCredentials
.
If either of these files does not exist, throw an error with the message 'The app "${appId}" in catalog "${catalogId}" could not be loaded because its credentials.json file does not exist' or 'The app "${appId}" in catalog "${catalogId}" could not be loaded because its install.xml file does not exist'
If the credentials file cannot be parsed as JSON, throw an error: 'The app "${appId}" in catalog "${catalogId}" could not be loaded because its credentials.json file could not be parsed'
You'll be editing server/Store/index.js
You're going to be doing two things:
First, for each app, now also serve the app's icon just by calling the helper you wrote (server/Store/helpers/serveIcon.js
) with the appropriate args
Second, serve the store logo by calling your helper (server/Store/helpers/serveStoreLogo.js
) with this.storeMetadata.logoFullPath
as the argument.
https://reactjs.org/tutorial/tutorial.html
Remember to follow all our rules.
Add a prop called onClick
and if it is defined (this is an optional prop with default null
), then add an onClick to the whole div.
Also, turn the AppTitle
into a button, and also pass the onClick
into the title. If there is an onClick
, then add the onClick
to the button. If there is no onClick
, then set the button's pointerEvents: false
so that the button is non-interactable.
You'll be editing server/detectCatalogAndPermissions.js
Before getting started, take a look at the following: docs/types/Catalog
for a description of the Catalog object, visit bit.ly/caccl-api for the definition of the api
object (specifically check out api.course.get
and api.account.get
).
In Canvas, there are "accounts." You can think of an account as a set of courses. Thus, each course belongs to a single account. Notice that each Catalog has a list of accounts that it applies too. If you launch the app store from a course within any of those accounts, then the user will be shown that Catalog.
You have two goals in this task. The first goal is to figure out which catalog the user should be shown (find the catalogId). The process you'll probably want to follow is:
await api.course.get({ courseId: <put course id here> })
to get info on the courseYour second goal is to determine if this user is an admin for this catalog. The user is an admin if they are able to get info on one of the accounts listed in the catalog. In other words, if api.account.get
returns something and doesn't result in an error for one of the accounts in the catalog, that person is an admin. To be even clearer, one by one, go through each account in the catalog. If we're able to get info on one of these accounts (no error), then the person is an admin. Start with the account that the course is in. If that fails, continue with the rest of the accounts in the catalog (any order is okay as long as you don't check the account that the course is in twice).
Finally, return an object { catalogId, isAdmin }
.
This is an endpoint on the server that's exposed to the client. This is not designed to be a public endpoint (e.g. /about
or /contact-us
). Instead, this is just for requesting data from the server.
You will be editing /routes
and implementing the /catalog
route.
Add the route app.get('/catalog', (req, res) => { ... });
The route will call store.getCatalogAndPermissions(api, launchInfo)
where api is req.api
and launchInfo is req.session.launchInfo
. If either of these objects does not exist, respond with an error: 'We could not load your customized list of apps because we couldn't connect to Canvas and process your launch info. Please re-launch. If this error occurs again, contact an admin.'
This endpoint will respond with the following object:
{
success: true/false,
message: <error message if success is false>,
catalog: <catalog metadata if success is true>,
isAdmin: <included if success is true>,
}
If the error has a .code
field, you can use the message of the error err.message
. Otherwise, print the error to the console.log
and return a generic "An unknown error occurred while getting the list of apps in the current catalog. Please contact an admin." message.
Now, also load app icons. Each app will have an icon either named icon.png
or icon.jpg
. Use fileExists
to see which one exists and just use the one that's there.
If neither file exists AND we aren't extending a parent, throw an error.
Add this to the App as app.icon
in the form:
app.icon = {
fullPath: <the full path to the icon>,
};
Your job is to implement the Store class in server/Store/index.js
. This class's job is to handle all of the loading and logic around catalogs and metadata.
The Store class will have a few methods:
_attemptLoad()
Function that attempts to perform a load. If successful, swaps out our metadata objects. If failed, leaves current metadata objects as is.
loadStore.js
)storeMetadata
– the store metadata object
installData
– a mapping { catalogId => appId => { installXML, installationCredentials } }
accountIdToCatalogId
– a mapping { accountId => catalogId }
taken straight from each catalog's accounts
field
catalogIdToCatalogMetadata
– a mapping { catalogId => catalog object }
where all apps in catalog.apps
have their installXML
and installationCredentials
removed
If all of this goes successfully, serve the screenshots then swap out this.storeMetadata
, this.installData
, this.accountIdToCatalogId
, and catalogIdToCatalogMetadata
.
To serve the screenshots, use the helpers/serveScreenshots.js
function to serve each app's screenshots (one by one). You'll need to pass in this.expressApp
in addition to the relevant ids and app metadata object. This helper function gives you a new app metadata object. Make sure you replace your current copy of the app metadata with the one returned from this helper.
getCatalogAndPermissions(api, launchInfo)
A function that simply calls server/helpers/detectCatalogAndPermissions
with the given arguments but also adds this.catalogIdToCatalogMetadata
as a third argument. Then, it uses the returned catalogId
field to look up the catalog metadata object and returns the following object:
{
catalog: <catalog metadata object>,
isAdmin: <same boolean we got from the helper>,
}
getInstallData(catalogId, appId)
A function that looks up the install data, adds in some app metadata, and returns it. Throw an intuitive error if the catalog or app does not have installData. This is the form of the object you'll be returning:
{
name: <title from app metadata>,
description: <subtitle from app metadata (NOT APP DESCRIPTION)>,
key: <consumer_key from installationCredentials inside installData>,
secret: <consumer_secret from installationCredentials inside installData>,
xml: <installXML from installData>,
launchPrivacy: <launchPrivacy from app metadata>,
}
getStoreMetadata()
A function that returns the store metadata.
This is an endpoint on the server that's exposed to the client. This is not designed to be a public endpoint (e.g. /about
or /contact-us
). Instead, this is just for requesting data from the server.
You will be editing /routes
and implementing /store
, which returns the store metadata.
Add the route app.get('/store', (req, res) => { ... });
To do this, you'll use the store's getStoreMetadata
function.
Send the user the following object using res.json(...)
:
return res.json({
success: true/false,
message: <error message>,
store: <store metadata>,
});
How do we know if there's an error (no success)? Put everything before it in a try/catch and if an error occurs, you know. Also, if store metadata is undefined/null, success is false and message is "The app catalog is not ready yet. Please check back in a few minutes. If this error continues to occur, contact an admin."
If the error has a .code
field, you can use the message of the error err.message
. Otherwise, print the error to the console.log
and return a generic "An unknown error occurred while getting the store metadata. Please contact an admin." message.
You will be editing server/Store/helpers/callOnSchedule.js
Big picture goal: every x seconds, attempt to reload the store. If an error occurs, revert back to the previous version of the store.
You will need to get a library that allows you to schedule tasks every minute.
You'll be editing: test/selenium/integration/server/appLifecycle.js
Find docs on the driver
here: bit.ly/dce-selenium
Before running the tests, you need to start the server. Here's how:
Open two tabs, and in the first one, run npm run dev:canvas
. In the second, run npm run dev:server-installable
.
Now we can run tests.
To run the tests, in a third tab, use npm run selenium
.
You'll be editing an integration test file: /test/selenium/integration/server/catalogEndpoint.js
See comments in that file for more details. Also see dce-selenium docs for help: bit.ly/dce-selenium
Before running the tests, start the server to test:
Open two tabs, in the first tab run npm run dev:canvas
and in the second tab, run npm run dev:server-installable
To run the tests, use:
npm run selenium
Add isInstalled
as a prop and add onSupportClicked
and onInstallClicked
and onUninstallClicked
as props, call those functions when the buttons are clicked.
Maybe switch to using a flex box:
Pseudocode:
<flex>
<div>Number</div>
<div w/ flex-grow>Description</div>
</flex>
You'll be editing server/routes.js
to add a DELETE /uninstall
endpoint.
Given the app's lti ids (get them using req.body.ltiIds
), use await req.course.app.remove({ ... })
to uninstall each app then send a response: { success: true/false, message: error message if success is false }
. Use return res.json(...)
to send the response.
If the error we catch has a err.code
field, send err.message
to the user. If not, tell the user: An unknown error occurred while attempting to uninstall an app. Please contact an admin.
This endpoint will be a simple POST
endpoint that installs an app into a course.
You will be editing server/routes.js
and will be adding a /install/:appId
route.
Use req.params
to get the appId
from the url: req.params.appId
.
When a user visits that route, we get the current catalogId stored in req.session.catalogId
, we use store.getInstallData
to get the install data, add a new courseId
parameter using the value in req.session.launchInfo.courseId
. Then, install the app using req.api.course.addApp
(see bit.ly/caccl-api for more info).
Then, respond to the client with the following body:
{
success: true/false,
message: 'Human-readable message upon failure',
}
If the error has a .code
field, you can use the message of the error err.message
. Otherwise, print the error to the console.log
and return a generic "An unknown error occurred while installing this app. Please contact an admin." message.
You'll be editing /test/selenium/integration/server/storeEndpoint.js
Find docs on the driver
here: https://harvard-edtech.github.io/dce-selenium/
Before running the tests, you need to start the server. Here's how:
Open two tabs, and in the first one, run npm run dev:canvas
. In the second, run npm run dev:server-installable
.
Now we can run tests.
To run the tests, in a third tab, use npm run selenium
.
This is a pretty simple task. You'll be editing/server/Store/index.js
to add hot reloading.
callOnSchedule
functionconst hotReload = () => {
this._attemptLoad();
};
STORE_CONSTANTS.js
file: hotReloadSecs
and set it to 60hotReload
function every STORE_CONSTANTS.hotReloadSecs
secondsThe toughest part of this task is testing: you'll need to copy a test store folder, modify it before the hot reload, and check to make sure the changes are reflected, then break the store metadata, make sure it didn't get loaded, then fix it and make sure it did get loaded, set beingEdited: true
in the storeMetadata
and make some other change, wait for the hot reload and make sure the store didn't get updated (that other change didn't make it into the Store class).
You'll be editing server/Store/serveStoreLogo.js
Your job will be to take the full path of the logo and serve it at /public/logo
using the usual techniques
This helper will post process an entire catalog, performing two key operations: adding a tagColors
object to the catalog if it doesn't have one, and adding "other/uncategorized" to apps that don't have any items under a tagName.
This helper will post process an entire catalog. It will:
Build the Catalog object completely in this format:
tagsToShow: [ { tagName: 'cost', tagColor: 'blue', }, { tagName: 'type', tagColor: 'red', }, ]
If the catalog doesn't have a tagColors
object, add one. The keys should be the set of keys from all of the apps. The values should be cyclicly chosen from the following list of colors:
ff2600
008e00
008e00
9437ff
ff2f92
ff9300
d783ff
005392
929000
tagsToShow
object, add ['other/uncategorized']
as its item array.You'll be editing server/Store/helpers/loadStore.js
.
See the TODO
and slack with questions
You will be editing client/src/utils/filter/genTagValueCounts.js
.
Count is calculated by:
Find the list of apps that would show up if the other filter categories are left in their current state and the current filter category is all checked
Within this list of filter items in this filter category, for each filter item:
a. Find the number of apps in the list from step 1 that have the current filter item’s tag
b. Write than number in the “count” box
Please slack with any questions.
Watch cs50's react tutorial and follow along, but read my comments as you go:
https://gist.github.com/gabeabrams/d19a4ff921e0d78011d629063aac4f78
You will be editing server/routes.js
.
This /installed-apps
pulls the list of LTIs in a course, matches them with apps in the catalog, and returns a list of installed apps with both their LTI id and their appId.
req.session.catalogId
Store.getCatalog
(if no catalog returned (undefined or null), send error: "We could not get your list of currently installed apps because your session has expired. Please re-launch from Canvas. If this issue continues to occur, please contact an admin.")req.session.launchInfo.courseId
(surround with try catch, send error: "Please re-launch this app from Canvas to continue. If this continues to occur, please contact an admin")await req.api.course.app.list(...)
ltiApp
), compare the following with the list of apps in the catalog (call it catalogApp
) to determine matches:ltiApp.privacy_level
should match with catalogApp.launchPrivacy
ltiApp.consumer_key
should match with the app's key (use store.getInstallData(...)
)ltiApp.name
should match with catalogApp.title
ltiApp.url
should be inside of the app's xml (use store.getInstallData(...)
)Finally, return a list of matches. If an app does not match, do not include it.
Return the following object:
{
success: <true/false>,
message: <error message if success is false>,
apps: [
{
ltiIds: array of ids from Canvas,
appId: the app's app store id,
},
...
],
}
You will be editing server/Store/helpers/serveIcon.js
.
This task will be very similar to the task where we served the screenshots.
Your job will be to take the app's icon (its file path will be at app.icon.fullPath
) and serve it at /public/<catalogId>/<appId>/icon
and then add this url at app.icon.url
.
Finally, edit server/Store/index.js
, specifically the _attemptLoad
function:
In the loop where you're editing each app, add one more step: call serveIcon
with the app and then remember to reassign the app:
const appWithServedIcon = serveIcon(app);
If in an app's metadata.json
file the creator
field is not an array, turn it into a single element array:
"creator": "DCE"
becomes
app.creator = ['DCE']
Implement the following files:
client/src/utils/filter/filterByQuery.js
If query is empty or just whitespace, return all apps
Otherwise, search the following fields. If any match, keep the app:
client/src/utils/filter/filterByTags.js
tags is an object of form:
{
tagName => {
color: 'blue',
tagValues: tagValue => isChecked,
}
}
For each app: keep it if for each tag in tags, the app has one of the checked itemNames in apps[i].tags[tagName]
client/src/utils/filter/excludeTagName.js
Given a tags object and a tagNameToExclude, remove the tagNameToExclude from the mapping.
By remove, I mean that the key and its value should be deleted. Example:
const tags = {
cost: ...,
type ...,
};
If tagNameToExclude is "cost", the resulting tags object returned is:
const tags = {
type ...,
};
Start by refreshing your Express.js knowledge on serving static files.
You'll be editing server/Store/helpers/serveScreenshots.js
The function takes two arguments: an express app and an app metadata object (see docs/types/App
for more info).
Each app object might have an app.screenshots
property. If it does not have this property, you have nothing to do. If it does have this property, you'll want to loop through all the screenshots and do the following:
Serve app.screenshots[i].fullPath
at the following path: /public/<catalogId>/<appId>/screenshots/<filename>
If the file does not exist, create a pretty error and throw it. Example message "The app '' in catalog '' listed a screenshot with filename '' but that file does not exist"
Add a url
property to each screenshot that holds the url above.
Return the updated app.
Reminder: you are not allowed to edit function arguments (you need to reassign the app before updating it)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.