15. Client-Side Framework¶
The client-side framework for OpenDSA is implemented in 3 main files
located in the lib directory: odsaMOD.js
, odsaAV.js
and
odsaUtils.js
.
odsaMOD.js
contains the code which is specific and common to
modules, while odsaAV.js
contains code specific and common to
AVs.
odsaUtils.js
contains several utility functions as
well as the interaction data logging functions used by both
odsaMOD.js
and odsaAV.js
.
AVs must include both odsaUtils.js
and odsaAV.js
(in that
order) to function properly as part of OpenDSA.
While the script links for modules are automatically appended during
the configuration process, modules must include _static/config.js
,
odsaUtils.js
and odsaMOD.js
(in that order).
config.js
is a file generated by the configuration
process that stores configurable settings needed by the client-side
framework.
Note: AVs should not include odsaMOD.js
and
modules should not include odsaAV.js
.
Be aware that modules and AVs actually load the minimized version of
these files (such as odsaAV-min.js
).
So if you edit one of the library files, be sure to run:
make min
from the OpenDSA toplevel before you check to see the effect. Note that
the minify process can be a fake minify (only copying the files) depending
on the Makefile variables. See the beginning lines of OpenDSA/Makefile
to
update this to your preference.
In order to allow AVs to be reused outside of OpenDSA (without including
OpenDSA infrastructure), odsaAV.js
checks for all dependencies provided
by odsaUtils.js
and provides default values or function stubs to
maintain its independence.
Additionally, AVs should not be made dependent on anything generated
by the configuration process (such as config.js
).
The client-side framework is responsible for:
- Allowing users to login, logout, and register new accounts
- Dynamically resizing the iFrames for embedded exercises
- Sending the information necessary to store a new exercise in the database
- Managing a user’s score
- Automatically buffering and sending score data to the backend server when a user completes an exercise
- Managing a user’s proficiency
- Determining whether a user obtains proficiency with a module or exercise
- Caching the user’s proficiency status locally
- Displaying the appropriate proficiency indicators
- Making sure the local proficiency cache remains in sync with the server
- Keeping multiple OpenDSA pages in sync
- Ensure that actions such as logging in, logging out or gaining proficiency are reflected across all OpenDSA pages open within the browser
- Collecting and transmitting user interaction data to the backend server
- Dynamically configuring the natural language, code language, and default parameters of AVs at runtime
All but the last two of these responsibilities are handled by
odsaMOD.js
, effectively making module pages the “brains” of the
client-side framework. We wanted the client to be able to determine
whether or not a user obtained proficiency with an exercise so that
OpenDSA could function without a backend server, but in order to do
so, the client needs to know the exercise’s threshold value. The
threshold is configurable and we didn’t want to compile AVs because it
would raise the barrier of entry for AV development (which currently
only requires a text editor and a browser), so the solution was to
include the threshold (and other exercise-specific information) on the
module page when it was built. The next challenge was to get the
score from embedded AVs to the module page which was easily
implemented using HTML5 postMessage. While the current configuration
system could send the threshold to embedded AVs as a URL parameter, we
choose to leave the system the way it is because it makes sense to
only grade an exercise within a specific context. Exercises don’t
intrinsically have points or a proficiency threshold. These are
properties of the context in which the exercise is viewed (i.e. they
are book dependent). Therefore it makes sense to have exercises
graded by code on the module page and to have module pages handle
submitting a user’s score and displaying an indicator of their
proficiency.
Each of the main JavaScript files are wrapped in anonymous functions to hide their internal variables and functions. Public functions are accessible through the global ODSA object. Global settings can be accessed through ODSA.SETTINGS, public utility functions can be accessed through ODSA.UTILS, public module functions through ODSA.MOD and public AV functions through ODSA.AV.
15.1. Responsibilities¶
15.1.1. Login, Logout and Registration¶
Unlike AVs and Khan-Academy exercises, module pages include links to login, logout and register a new account. The HTML for the login and registration boxes can be found in ~OpenDSA/RST/source/_themes/haiku/layout.html
and, of course, the code for it can be found (clearly marked) in ~OpenDSA/lib/odsaMOD.js
. showRegistrationBox()
and showLoginBox()
make the respective pop-up boxes appear and ensure they are centered correctly on the page, while hidePopupBox()
hides which ever pop-up box is showing. login(username, password)
sends an AJAX request to the server with the provided username and password and if successful will create a new session for the given user. When the page loads, click handlers are attached to the various login and registration links and buttons to trigger the appropriate behavior. Clicking the “Login” or “Register” links will open the appropriate pop-up box, clicking “Submit” in the login box will trigger the login(username, password)
, and clicking “Submit” in the registration box will cause the user’s input to be validation and if successful an AJAX message is sent to the server requesting a new user account. If the user’s input is invalid an error message is displayed and if registration is successful login(username, password)
is called to automatically log the user in.
Since the database only handles one session per user at a time, if the user logs in anywhere else, their first session is invalidated. If the user triggers any communication to the server using the old, invalid session key, the server will return an HTTP 401 error causing the framework to call handleExpiredSession(key)
. This function removes the old session information from local storage, informs the user that their session is no longer valid and that they must log in again, then refreshes the page when the user closes the message which causes the login box to pop up. The module page also implements a listener for “odsa-session-expired” events which are generated by the event logging functions in odsaUtils.js
if they receive an HTTP 401 error. When these events are received they call handleExpiredSession(key)
.
If no backend server is enabled, the login and registration links are hidden because they serve no function if there is no server to log into.
15.1.2. Dynamic iFrame Resizing for Embedded Exercises¶
The client-side framework supports changing the height and width of embedded exercise iFrames at runtime. odsaAV.js
sends an HTML5 postMessage to the parent module page when the AV loads or is reset, communicating the height and width of the rendered page. A listener defined in odsaMOD.js
receives the message and updates the dimensions of the iFrame associated with the exercise and hiding the iFrame, if applicable. Note: If the iFrame is hidden when the exercise is loaded, the dimensions may not be reported properly, so the iFrame must be hidden after it has been loaded and resized.
Due to the way Khan-Academy exercises can contain multiple problems of different sizes, the overall exercise must use data attributes of the exercise’s body element to define the largest necessary height and width, as seen in the following example:
<body data-height="650" data-width="950">
<div class="exercise" data-name="ExchangeTF1"></div>
...
These data attributes are read from the Khan-Academy exercise file by the avembed
directive during the compilation process and used to set the dimensions of the exercise’s iFrame.
15.1.3. Dynamically Loading Exercises¶
One advantage to having all the configuration information for modules and exercises available on the client is that it provides an easy way to load exercises into the database that do not already appear there. A function called loadModule()
is called when a page loads which handles several conditions. If a user is logged in, it sends an AJAX request to the server which contains enough information to load the module and all the exercises it contains if they do not already exist in the database. The response from the server contains information about the user’s proficiency with the module, each exercise in the module and progress information for the Khan Academy-style exercises. The local proficiency cache is updated based on the information in the response which keeps the client in sync with the server. If no user is logged in when loadModule()
is called, the anonymous (guest) user information stored in the local proficiency cache is used to initialize the proficiency indicators on the module page.
15.1.4. Score Management¶
Module pages contain 3 listeners. One listens for “jsav-log-event” events which are generated by the JSAV-based mini-slideshows that are included on most module pages, while a second listens for HTML5 postMessages from embedded AVs or Khan Academy exercises. The third is not relevant to this section and is described above (see Login, Logout and Registration). The first two listeners call processEventData(data)
which performs some processing to make sure all additional event data is logged properly and calls storeExerciseScore(exercise, score, totalTime)
under 3 circumstances: if the event type is “odsa-award-credit”, if the user has reached the end of a slideshow (and all the steps were viewed or the book is configured to allow credit without viewing all the slides), and if the event type is “jsav-exercise-grade-change” and the final step in the exercise was just completed. If a user is logged in or the system is configured to assign anonymous score data to the next user who logs in storeExerciseScore()
will create a score object and store it in local storage in accordance with the `Score Data`_ model below. If the score is above the proficiency threshold and either no backend server is enabled or no user is logged in, the anonymous (guest) user is awarded proficiency and the appropriate proficiency indicator is displayed.
JSAV does not communicate directly with the OpenDSA backend and does not tell the backend to award credit for an exercise. In the case of proficiency exercises, JSAV generates a “jsav-exercise-grade-change” event which contains the student’s points and the total number of points for the exercise. The OpenDSA client-side framework calculates the student’s score and compares it to the threshold value for the exercise that was provided in the configuration file. If the score is greater than or equal to the threshold, credit is awarded locally. The calculated score is packaged up and sent to the backend which makes an independent comparison to the threshold that has previously been sent by the client and verifies whether or not the student should obtain credit.
Some OpenDSA functions such as awardCompletionCredit()
and logExerciseInit()
generate events on the same channel as JSAV in order to make use of the existing listener. While they communicate on the same channel, these functions are not associated with JSAV and are NOT dependent on the JSAV framework.
Near the end of processEventData()
, flushStoredData()
is called which in turn calls sendExerciseScores()
and sendEventData()
(which is defined in odsaUtils.js
). sendExerciseScores()
loops through local storage calling sendExerciseScore()
for any score events with a timestamp less than the timestamp taken when the function was called. sendExerciseScore()
sends the specified score object to the backend server and updates the user’s proficiency status for the exercise based on the server’s response. If the score was sent successfully or was rejected by the server, the object is removed from local storage. In the case of rejection, the data is removed to prevent a build up of bad data that will never succeed and be cleared. If transmission is unsuccessful for another reason, the score object will remain in local storage and the framework will attempt to send it again in the future.
15.1.5. Proficiency Management¶
The module page is also in charge of determining a user’s proficiency with an exercise or module, caching this proficiency status in local storage, displaying the appropriate proficiency indicator for each exercise and making sure the local proficiency cache stays in sync with the server. For each book, for each user, the client stores the status of each exercise with which the user obtains proficiency. The status can be one of several states:
- SUBMITTED - indicates the user has obtained local proficiency and their score has been sent to the server
- STORED - indicates the user has obtained local proficiency and the server has successfully stored it
- ERROR - indicates the user has obtained local proficiency, the score was sent to the server but it was not stored successfully
- If an exercise does not appear in a user’s proficiency cache, that user has not obtained proficiency
15.1.5.1. Local Proficiency Cache¶
The primary purpose of the local proficiency cache is to allow anonymous (guest) users to maintain their progress and to allow OpenDSA to function without a backend server, but a secondary purpose is to make pages more responsive for logged in users. While loadModule()
(which is called on every page when a user is logged in) returns the user’s proficiency information, keeping a local copy allows the page to immediately display the proper proficiency indicators rather than waiting for a response from the server. See `Proficiency Data`_ for information about the format of the cached data.
15.1.5.2. Proficiency Displays¶
Proficiency for mini-slideshows is indicated by the appearance of a green checkmark on the right side of the slideshow container. If the status is SUBMITTED
, a “Saving…” message will appear beneath the checkmark but will be hidden once the status changes to STORED
. If the status is set to ERROR
, a warning indicator will appear (to draw the user’s attention to the exercise) and the saving message will be replaced by an error message and a “Resubmit” link which allows the user to resend their score data without recompleting the exercise.
Proficiency for embedded exercises is indicated by the color of the button used to show or hide the exercise. Red indicates the user is not proficient, yellow indicates the user’s score has been submitted or an error occurred and green indicates that the user is proficient (and their proficiency has been verified by the server).
When a user obtains proficiency for all the required exercises in a module, the words “Module Complete” will appear in green at the top of the module. If “Module Complete” appears in yellow, the user has obtained local proficiency with all the required exercises but one or more of them have not yet been successfully verified by the server (this should ONLY appear when a user is logged in). In general, to obtain module completion a user must complete all exercises marked as “required” in the configuration file. If a module does not contain any required exercises, module completion cannot be obtained unless the configuration file sets “dispModComp” to “true” for the given module. Inversely, if “dispModComp” is set to “false” module completion will not be awarded even if the user completes all the required exercises.
On the Contents (index) page, a small green checkmark next to a module indicates that it is complete.
On the Gradebook page, the score for exercises and modules with which the user is proficient are highlighted in green. At this time, there is no concept of chapter completion.
All updates to proficiency displays are handled by updateProfDisplay()
. Code within the function determines what displays exist for the given exercise or module and updates them according to the associated status stored in the local proficiency cache.
15.1.5.3. Syncing with the Server¶
As described above, under Dynamically Loading Exercises, loadModule()
is called when each module page loads and the response contains information about the user’s proficiency with the module and each exercise in the module.
The Contents (index) and Gradebook pages call syncProficiency()
which initiates an AJAX request to the backend server which in turn responds with the proficiency for all modules and exercises.
In both cases, the information returned by the server is used to update the local proficiency cache.
15.1.5.4. Determining Proficiency Status¶
Proficiency status is determined differently in different situations. If no backend server is enabled or no user is logged in (meaning the user is anonymous / guest), the client is given the authority to determine whether or not a user is proficient with an exercise or module. Exercise proficiency is awarded if the user’s score on an exercise is greater than or equal to the proficiency threshold for that exercise. Module proficiency is awarded when a user has obtained proficiency with all exercises in a module that are listed as “required” in the configuration file. Since there is no server involved in the process, the only valid status for anonymous (guest) users is STORED
.
The backend server is required to verify proficiency of all logged in users and two additional statuses are added to handle interaction with the server. When a logged in user’s exercise score is sent to the server, if the client determines they are proficient, their status for the given exercise is set to SUBMITTED
. When the server responds to the AJAX request, the response contains a boolean indicating whether or not the user is proficient with the given exercise. If the server determines the user is proficient, their status for the exercise is set to STORED
, but if the server responds with "success": false
or an HTTP error occurs, the status is set to ERROR
.
When the status of a required exercise is set to STORED
(in storeStatusAndUpdateDisplays()
), the framework calls checkProficiency(moduleName)
to check for module proficiency. checkProficiency()
begins by calling updateProfDisplay()
which updates the proficiency displays for the given exercise or module based on the contents of the local proficiency cache and returns the status. If the status is STORED
, checkProficiency()
returns immediately. If the status is not STORED
but a user is logged in, the framework will send an AJAX request to the backend server asking if the user is proficient with the exercise or module and update the proficiency cache appropriately when it receives a response. If the status is not STORED
, no user is logged in and the request is for module proficiency, checkProficiency()
will loop through the exercises
object (see Exercises) and determine if the anonymous (guest) user has proficiency with all required exercises. If so, the guest account is awarded module proficiency and the cache is updated. If a single required exercise is found that the guest user is not proficient with, the loop short circuits and the function returns.
A user’s proficiency status can also be updated by the synchronization functions loadModule()
and syncProficiency()
(see Syncing with the Server).
15.1.6. Keeping Pages in Sync¶
Consider the situation where a user logs in to OpenDSA and then opens modules in multiple tabs. Since a user is logged in each tab will display the logged in user’s name in the top right hand corner. Later, the user logs out and another user logs in on one of the pages. Without a system to sync pages, it would appear as if two users are logged in at the same time which could potentially be very confusing. To rectify this situation, odsaMOD.js
implements an updateLogin()
function which is called any time the window receives focus. The purpose of this function is to determine whether or not the current user appears to be logged in and if not to fix it. If another user has logged in since the page was loaded, the former user’s name is replaced with the current user’s name and if no user is logged in, the logout link and former user’s name are replaced with the default “Register” and “Login” links. If any change is made, loadModule()
is called to ensure the proficiency displays match the current user’s progress. Since the function is called when the window receives focus, updates will be made as soon as the user clicks on the tab to open it.
15.1.7. Interaction Data Collection and Transmission¶
We collect data about how users interact with OpenDSA for two reasons
- To continually improve OpenDSA
- For research purposes
As a user interacts with OpenDSA, a variety of events are generated. If there is a backend server enabled, we record information about these events, buffering it in local storage and sending it to the server when a flush is triggered. If a user is logged in, we send the event data with their session key, effectively tying interaction data to a specific user, but if no user is logged in the data is sent anonymously (using ‘phantom-key’ as the session key). This ensures that we are able to collect as much interaction data as possible.
15.1.8. Runtime Exercise Configuration Support¶
The client-side framework supports limited dynamic configuration at runtime through the use of JSON exercise configuration files (not related to the JSON config file used by the configuration system). Configuration currently supports:
- Natural language switching
- Code language switching
- Default parameter configuration
While the natural language of module text and code language of code snippets are set by the configuration system when the book is built (by the configuration system and codeinclude directive, respectively), exercises and some interface elements are (intentionally) not altered by the configuration process and therefore must be configured at runtime. We take extreme measures to keep from having to alter the exercises during the configuration process so that we can load the same exercise on different book instances and to lower the barrier of entry for new AV developers so that the only tools they need are a text editor and a browser rather than our entire tool chain.
The function responsible for this is loadConfig()
in odsaUtils.js
. It uses AJAX to load the appropriate JSON exercise configuration file, does additional processing and loading as needed to obtain the natural language translations, applies translated labels to interface elements, loads the appropriate code snippet if it exists, and applies the default parameters from the configuration file to the PARAMS
object such that any conflicting parameters are overridden unless the parameter is set via the URL.
15.1.8.1. JSON File Locations¶
The framework assumes that standalone AVs and mini-slideshows follow the convention of having a config file [av_name].json in the same directory as the JS file the defines the AV, if the path to the JSON file is different (in a different directory, a common JSON file is shared between AVs, etc), the path relative to the OpenDSA root directory must be specified using the “json_path” argument. Example: ODSA.UTILS.loadConfig({"json_path": "AV/Sorting/shellsortAV.json"});
The av_name
argument defaults to ODSA.SETTINGS.AV_NAME
which should work out of the box for all standalone AVs, but is not initialized on modules pages, making this argument required for mini-slideshows.
By convention the ID of the container containing the AV defaults to #container
for standalone AVs and #[av_name]
(auto-generated by the inlineav directive) for mini-slideshows, as long as you follow this convention, you should not have to provide this argument
15.1.8.2. JSON Format¶
The JSON exercise configuration file may contain the keys: translations
, code
, and params
. Each key under translations
should be an ISO-639 standardized language code. For keys beneath a language code key, if the key is prefixed with av_
it will be ignored by the framework and left up to the AV developer to explicitly reference it. All other keys will be evaluated as a jQuery selector and the associated string applied to the element returned.
Each key under code
corresponds to a programming language which must have a matching folder in the SourceCode/ directory. Note that while the directory in SourceCode/ may contain capitals, the key must be all lowercase. This standard was adopted to ensure consistent key names across AV authors (i.e. prevent one author from using Java
while another uses java
, etc)
{
"translations" : {
"en": {
".avTitle": "Insertion Sort Visualization",
"av_Authors": "Cliff Shaffer and Nayef Copty",
"#about": "About",
"#run": "Run",
"#reset": "Reset",
"#arraysizeLabel": " List size: ",
"#arrayValuesLabel": " Your values: ",
"av_arrValsPlaceholder": "Type some array values, or click 'run' to use random values",
"av_c1": "Starting Insertion Sort.",
"av_c2": "Done sorting!",
"av_c3": "Highlighted yellow records to the left are always sorted. We begin with the record in position 0 in the sorted portion, and we will be moving the record in position 1 (in blue) to the left until it is sorted.",
"av_c4": "Processing record in position ",
"av_c5": "Move the blue record to the left until it reaches the correct position.",
"av_c6": "Swap."
},
"fi": {
".avTitle": "Lomitusjärjestäminen",
"av_Authors": "Cliff Shaffer and Nayef Copty",
"#about": "Lisätietoa",
"#run": "Suorita",
"#reset": "Alusta",
"#arraysizeLabel": " Taulukon koko: ",
"#arrayValuesLabel": " Omat arvot: ",
"av_arrValsPlaceholder": "Erottele arvot välilyönnillä tai jätä tyhjäksi satunnaislukuja varten",
"av_c1": "FIStarting Insertion Sort.",
"av_c2": "FIDone sorting!",
"av_c3": "FIHighlighted yellow records to the left are always sorted. We begin with the record in position 0 in the sorted portion, and we will be moving the record in position 1 (in blue) to the left until it is sorted.",
"av_c4": "FIProcessing record in position ",
"av_c5": "FIMove the blue record to the left until it reaches the correct position.",
"av_c6": "FISwap."
},
"sv": {
".avTitle": "Visualisering av Mergesort",
"av_Authors": "Cliff Shaffer and Nayef Copty",
"#about": "Om",
"#run": "Kör",
"#reset": "Återställ",
"#arraysizeLabel": " Liststorlek: ",
"#arrayValuesLabel": " Dina värden: ",
"av_arrValsPlaceholder": "Skriv in dina värden eller lämna blankt för att använda slumpmässiga värden",
"av_c1": "SVStarting Insertion Sort.",
"av_c2": "SVDone sorting!",
"av_c3": "SVHighlighted yellow records to the left are always sorted. We begin with the record in position 0 in the sorted portion, and we will be moving the record in position 1 (in blue) to the left until it is sorted.",
"av_c4": "SVProcessing record in position ",
"av_c5": "SVMove the blue record to the left until it reaches the correct position.",
"av_c6": "SVSwap."
}
},
"code" : {
"processing": {
"url": "../../SourceCode/Processing/Sorting/Insertionsort.pde",
"startAfter": "/* *** ODSATag: Insertionsort *** */",
"endBefore": "/* *** ODSAendTag: Insertionsort *** */",
"lineNumbers": false,
"tags": {
"sig": 1,
"outloop": 2,
"inloop": 3,
"swap": 4,
"end": 5
}
},
"c++": {
"url": "../../SourceCode/C++/Sorting/Insertionsort.cpp",
"startAfter": "/* *** ODSATag: Insertionsort *** */",
"endBefore": "/* *** ODSAendTag: Insertionsort *** */",
"tags": {
"sig": 1,
"outloop": 2,
"inloop": 3,
"swap": 4,
"end": 5
}
},
"java": {
"url": "../../SourceCode/Java/Sorting/Insertionsort.java",
"startAfter": "/* *** ODSATag: Insertionsort *** */",
"endBefore": "/* *** ODSAendTag: Insertionsort *** */",
"tags": {
"sig": 1,
"outloop": 2,
"inloop": 3,
"swap": 4,
"end": 5
}
}
},
"params": {
"JXOP-lang": "en"
}
}
15.1.8.3. Control¶
Control over the natural language of an exercise is done by setting either JSAV_EXERCISE_OPTIONS.lang
or JSAV_OPTIONS.lang
, while the code language is controlled by JSAV_EXERCISE_OPTIONS.code
or JSAV_OPTIONS.code
.
For embedded AVs, these options can be set several different ways:
- Hardcoding a setting into the framework itself (rare)
- Using the
params
field of the exercise configuration file.- Using the
glob_exer_options
field in the book configuration file.- Using the
exer_options
field related to a specific exercise in the book configuration file.
Method 1 Example
JSAV_EXERCISE_OPTIONS.lang = 'en';
JSAV_EXERCISE_OPTIONS.code = 'c++';
Method 2 Example
{
...,
"params": {
"JXOP-lang": "en",
"JXOP-code": "c++"
}
}
Method 3 Example
{
...,
"glob_exer_options": {
"JXOP-lang": "en",
"JXOP-code": "c++"
},
...,
}
Method 4 Example
{
...,
"chapters": {
...,
"Algorithm Analysis": {
...,
"AlgAnal/AnalProgram": {
...,
"exercises": {
"binarySearchCON": {
"exer_options": { "JXOP-code": "none" },
...
},
}
},
...,
},
...,
}
}
For mini-slideshows, the first two methods from above apply, but options three and four use glob_mod_options
and mod_options
, respectively. See
Configuration for more information.
The order of precedence is such that the later methods will override the previous ones. If the preferred natural language is not present in the configuration file, the framework will default to English. If the preferred code language is not present, the framework will default to the first code language defined in the file. If the code language is set to none
or the code object is entirely omitted from the config file, then code display will be disabled for the AV.
15.2. Data Model¶
The following sections describe the format of different data structures used for the client-side framework.
15.2.1. Exercises¶
Each module page creates an exercises
object on page load which is used to quickly and easily access important information about the module’s exercises. Each exercise object in exercises
includes:
- Points - the number of points the exercise is worth
- Threshold - the minimum score a user must receive to obtain proficiency
- Type - the type of exercise
- ‘ka’ for Khan Academy style exercises
- ‘pe’ for proficiency exercises
- ‘ss’ for slideshows
- uiid (unique instance identifier) - a code that uniquely identifies an instance of an exercise, used to group log events
Example of exercises
{
"shellsortCON1": {
"points": 0.1,
"threshold": 1.0,
"type": ss,
"uiid": 1362467525562
},
"ShellsortProficiency": {
"points": 1.1,
"threshold": 0.9,
"type": pe,
"uiid": 1362467577655
}
}
15.2.2. Event Messages Transmitted to the Server¶
For details about event messages and their format that are transmitted by the client-side framework to the server, see Event Messages.
15.3. Implementation and Operation¶
With the exception of login, all data is sent to the server with a session key rather than the username. The server is able to recover the username from the session and this should prevent data from inappropriately being sent as a different user. Since anonymous users do not have sessions, their interaction data is sent using the hardcoded value, “phantom-key”, as the session key.
15.3.1. Data Flow¶
As a user interacts with an AV, it generates events. A listener in odsaAV.js
processes the events (logging additional event data in desc field, triggering certain AV specific events like displaying a message saying no credit will be given after viewing the model answer, etc), logs them and forwards the event to the parent page. The parent page may or may not implement an event listener and process them further (a flag is set to indicate the event has already been logged, to prevent duplicate logging). The module page implements such a listener and passes events from embedded pages and events generated by the module itself to processEventData()
. Here events which have not been logged are logged and certain events trigger saving a user’s score (namely moving forward to the last slide of a slideshow, completing a graded exercise, odsa-award-credit
event used to award completion credit). In these cases, storeExerciseScore()
is called to store the user’s score in localStorage with additional information about the exercise. At the end of processEventData()
, score and event data are pushed to the server, if necessary, using flushStoredData()
(which calls sendEventData()
and sendExercisesScores()
).
15.3.2. Page Initialization¶
updateLogin()
is called on page load or when the page gains focus and functions to ensure consistency between all OpenDSA pages, specifically making sure the current user appears logged in and the proficiency indicators display that user’s proficiency. Without this function, a user could log in to multiple tabs, then log out of one and still appear to be logged into the others or another user could log in and it would appear that two users were logged in on the same browser at the same time, even though all data would be submitted as the last user to log in.updateLogin()
synchronizes all the pages to prevent confusing situations.loadModule()
is called when the page loads and whenupdateLogin()
updates a page to reflect a new user being logged in and performs different actions in different contexts. If the user is on the index page,loadModule()
loops through all the linked module pages and calls checkProficiency() for each. If the user is viewing a module page, one of two things happens. If the backend server is enabled and a user is logged in, a message will be sent to the server containing all the information necessary to load the module and all exercises if they don’t already appear in the database and the response from the server will contain the user’s proficiency status which each exercise and the module itself (the progress is also returned which allows the client to update the progress bar on Khan Academy exercises). If no backend server is enabled or no user is logged in,loadModule()
updates the proficiency indicators based on the anonymous user’s data in local storage.
15.3.3. Support Functions¶
storeStatusAndUpdateDisplays()
calls storeProficiencyStatus()
to store the given status in the local storage, then updates the appropriate proficiency display (whether its for an exercise or a module) and checks whether or not the user is now proficient with the module (if the user just gained proficiency with an exercise)
storeProficiencyStatus(name, [status], [username])
takes an exercise or module name, a status (optional) and username (optional) and caches the given status for the given exercise / module for the given user in local storage. If username is not specified, the current user’s name is used and if status is not specified, it defaults toSTORED
.updateProfDisplay(name)
can be called with either an exercise or module name as an argument (if no argument is given, it will default to the current module name). The function automatically detects whether the argument is an exercise or module name and updates the appropriate display(s) based on the current user’s proficiency status in local storage.checkProficiency(name)
can be called with either an exercise or module name as an argument (if no argument is given, it will default to the current module name). This function checks local storage for the given exercise / module and if it’s found, callsupdateProfDisplay()
and returns. If the exercise / module is not found, the server is queried for the user’s proficiency status and when the response is received,storeStatusAndUpdateDisplays()
is called to make sure the status is stored in local storage and the proficiency indicators are updated.
15.4. Debugging¶
The client-side framework is a relatively complex system which can be difficult to fully understand without tracing its execution. While the debugging tool built into Firebug can be useful for this, its impossible to back up and see something execute again or compare how a value changes without manually remembering the previous value. The current solution is to wrap console logging statements with a conditional based on the flag localStorage.DEBUG_MODE
. To enable DEBUG_MODE simply run localStorage.DEBUG_MODE = 'true'
from the JavaScript console. The log statements are grouped by function and internal calls are nested to make it easy to trace the call chain. Groups can be collapsed to hide information the user is not interested in and make the interesting information stand out more. It also provides a quick and easy way for a developer to scan through the log and make sure all the functions they expect to be called are called without having to step through all of them with the debugger. To disable verbose logging, run: delete localStorage.DEBUG_MODE
from the JavaScript console.
Unfortunately, this debugging system makes the code a little more bulky and less readable, but it has been found to be very helpful for debugging. Additionally, if students are experiencing problems, this system will allow us to quickly and easily diagnose their problem on their own computer without requiring them to install Firebug or adding additional print statements to the framework itself.
15.5. MathJax Support¶
We use MathJax extensively to create mathematical expressions. It gets used in module text, and within AVs and Exercises. Proper use of MathJax involves providing it with necessary configuration, in addition to loading the necessary JavaScript library. Since OpenDSA must insure that this information gets to all of the necessary parts of the system, there are certain places where support has been embedded. This section attempts to document them.
First, a given HTML page will need to load the MathJax library.
Like all JavaScript libraries used by the system, these are enumerated
in tools/config_templates.py
, within the html_context
variable.
This will get it loaded into a module.
Standalone AV and exercise developers are responsible for explicitly
including it in their own HTML files if they want MathJax processing.
Next, MathJax will need some local configuration.
This is added to module pages from the module page template at
RST/_themes/haiku/basic/layout.html
.
Look for where it defines MathJax.Hub.Config
.
For standalone AVs and exercises, this is defined in
lib/odsaAV.js
.
[TODO: Add in information about how KA infrastructure loads its own
version of MathJax.]
Finally, in order to get MathJax translation to take effect within
JSAV-controlled elements, JSAV has to be told to fire the translation
on various events (jsav-message
and jsav-updatecounter
).
This has been defined in both odsaAV.js
and odsaMOD.js
.