Skip to content

Commit

Permalink
Merge pull request #92 from lumenlearning/rc/DL3-kalamazoo
Browse files Browse the repository at this point in the history
Release Candidate for Kalamazoo October 6 2022
  • Loading branch information
smith750 authored Oct 6, 2022
2 parents 8be45d4 + 3dcc9fe commit c28bf44
Show file tree
Hide file tree
Showing 32 changed files with 1,221 additions and 159 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,13 @@ lti13.enableDynamicRegistration=false
lti13.enableDeepLinking=false
lti13.enableRoleTesting=false
lti13.enableTokenController=false
lti13.enableMockValkyrie=false
```
- `lti13.demoMode` must have a value of `true` for AGS, Membership, or Deep Linking (the LTI Advantage Services) to function regardless of the value of their individual env variables. This is because each of these services is currently a demo.
- `lti13.enableAGS` must have a value of `true` in addition to `lti13.demoMode` having a value of `true` for the Assignments and Grades (AGS) demo service to function.
- `lti13.enableMembership` must have a value of `true` in addition to `lti13.demoMode` having a value of `true` for the Membership demo service to function.
- `lti13.enableDeepLinking` must have a value of `true` in addition to `lti13.demoMode` having a value of `true` for the Deep Linking demo service to function.
- `lti13.enableMockValkyrie` must have a value of `true` in order to do integration testing locally.
- The remaining new env vars `lti13.enableDynamicRegistration`, `lti13.enableRoleTesting`, `lti13.enableTokenController` are for test endpoints. `lti13.enableDynamicRegistration` corresponds to the `RegistrationController` which has not been tested. `lti13.enableRoleTesting` corresponds to the `TestController` which I suspect will be deleted as role authorization is not needed. `lti13.enableTokenController` corresponds to `TokenController` which I suspect will be deleted if it remains unused.

## Setup SQS for AGS Grade Pass Back
Expand Down Expand Up @@ -235,6 +237,7 @@ Please complete this properties in the `.properties`
```
harmony.courses.api=http://localhost:3000/lti/deep_linking
```
To do integration testing locally ensure that `lti13.enableMockValkyrie=true` and set the `harmony.courses.api` equal to your ngrok url with `/valkyrie` appended to the end. (e.g. `https://5f41-184-101-29-219.ngrok.io/valkyrie`)

## Dynamic Registration

Expand Down
4 changes: 3 additions & 1 deletion src/main/frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
clientId: '[[${clientId}]]',
iss: '[[${iss}]]',
context: '[[${context}]]',
root_outcome_guid: '[[${root_outcome_guid}]]'
root_outcome_guid: '[[${root_outcome_guid}]]',
platform_family_code: '[[${platform_family_code}]]',
lti_system_error: '[[${lti_system_error}]]'
};
<!--/* We have to set an attribute to the root element so the APP can use the values of the LTI launch data. */-->
document.getElementById('root').setAttribute('lti-launch-data', JSON.stringify(ltiLaunchData));
Expand Down
4 changes: 4 additions & 0 deletions src/main/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,7 @@ li:not(:last-child) {
max-width: 135px;
padding: 10px;
}

.loading-courses {
min-height: 340px;
}
55 changes: 38 additions & 17 deletions src/main/frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,69 @@ import { useSelector } from 'react-redux';
// Store imports
import {
selectSelectedCourse,
selectLoading
selectLoading,
selectLtiSystemError
} from './app/appSlice';

// Components import
import Container from 'react-bootstrap/Container';
import CoursePreview from './features/CoursePreview';
import CoursePicker from './features/CoursePicker';
import LoadingPage from './features/LoadingPage';
import ErrorAlert from './features/alerts/ErrorAlert';
import LoadingCoursesPage from './features/LoadingCoursesPage';
import LtiBreadcrumb from './features/LtiBreadcrumb';

// Stylesheet imports
import './App.css';
import "@fontsource/lato";
import "@fontsource/public-sans";

// Utils
import { LTI_SYSTEM_ERRORS } from './util/LtiSystemErrors';

function App() {

// The window will never notice if the user is browsing in long contents or not, should always scroll to top when navigating across courses.
window.scrollTo(0, 0);

const selectedCourse = useSelector(selectSelectedCourse);
const ltiSystemErrorCode = useSelector(selectLtiSystemError);
const isLoading = useSelector(selectLoading);

// Show the course picker by default
let appContent = <CoursePicker />;
// When there's a system error from the backend we must inform the user and do not render any content.
if (Number.isInteger(parseInt(ltiSystemErrorCode))) {
let systemErrorMessage = null;
switch (parseInt(ltiSystemErrorCode)) {
case LTI_SYSTEM_ERRORS.DYNAMIC_REGISTRATION_GENERAL_ERROR:
systemErrorMessage = `Oops something went wrong with the Lumen Dynamic Registration. Please contact Lumen Support.`;
break;
case LTI_SYSTEM_ERRORS.DYNAMIC_REGISTRATION_DUPLICATE_ERROR:
systemErrorMessage = `Oops something went wrong. It appears you already have a tool registered with this Lumen domain.`;
break;
case LTI_SYSTEM_ERRORS.LINEITEMS_SYNCING_ERROR:
systemErrorMessage = `Oops, something went wrong and we couldn't load your content. Please try again. If the issue persists, please contact Lumen Support.`;
break;
default:
systemErrorMessage = `Unrecognized error message. Please try again. If the issue persists, please contact Lumen Support.`;
break;
}
return <Container className="App" fluid role="main">
<div className="mt-3"><ErrorAlert message={systemErrorMessage} /></div>
</Container>;
}

// Display a spinner when the app is loading data from the backend....
// When courses are being loaded display a loading page.
if (isLoading) {
return <LoadingPage />;
// When a course has been selected display the course preview.
} else if (selectedCourse) {
appContent = <CoursePreview course={selectedCourse} />
return <LoadingCoursesPage />;
}

return (
<>
<LtiBreadcrumb course={selectedCourse} />
<Container className="App" fluid role="main">
{appContent}
</Container>
</>
);
// When a course has been selected display the course preview.
return <>
<LtiBreadcrumb course={selectedCourse} />
<Container className="App" fluid role="main">
{selectedCourse ? <CoursePreview course={selectedCourse} /> : <CoursePicker />}
</Container>
</>;

}

Expand Down
6 changes: 6 additions & 0 deletions src/main/frontend/src/app/appSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const appSlice = createSlice({
setLoading: (state, action) => {
state.loading = action.payload;
},
setIsFetchingDeepLinks: (state, action) => {
state.isFetchingDeepLinks = action.payload;
},
setCourseArray: (state, action) => {
state.courseArray = state.filteredCourseArray = action.payload;
},
Expand Down Expand Up @@ -68,6 +71,7 @@ export const {
setErrorFetchingCourses,
setCourseArray,
setLoading,
setIsFetchingDeepLinks,
toggleAllModules,
updateMetadata
} = appSlice.actions;
Expand All @@ -78,6 +82,7 @@ export const selectSelectedCourse = (state) => state.selectedCourse;
export const selectCourseArray = (state) => state.filteredCourseArray;
export const selectMetadata = (state) => state.metadata;
export const selectLoading = (state) => state.loading;
export const selectIsFetchingDeepLinks = (state) => state.isFetchingDeepLinks;
export const selectErrorFetchingCourses = (state) => state.errorFetchingCourses;
export const selectErrorAssociatingCourse = (state) => state.errorAssociatingCourse;
export const selectSelectedModules = (state) => state.selectedModules;
Expand All @@ -90,6 +95,7 @@ export const selectIdToken = (state) => state.id_token;
export const selectState = (state) => state.state;
export const selectTarget = (state) => state.target;
export const selectRootOutcomeGuid = (state) => state.root_outcome_guid;
export const selectLtiSystemError = (state) => state.lti_system_error;

// This function fetches the courses from the backend, it should be invoked when loading the application.
export const fetchCourses = (page) => (dispatch, getState) => {
Expand Down
6 changes: 4 additions & 2 deletions src/main/frontend/src/features/CourseCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ function CourseCard (props) {

// Some courses may not have a valid cover image, use a default instead
const courseCoverUrl = parseCourseCoverImage(props.course.cover_img_url);
const courseTitle = props.course.book_title ? props.course.book_title : 'This course does not have a title.';
const courseCoverTitle = `The cover image for the ${courseTitle} course.`;

const changeCourseSelection = () => {
dispatch(changeSelectedCourse(props.course));
Expand All @@ -30,9 +32,9 @@ function CourseCard (props) {
tabIndex="0"
onKeyPress={(e) => checkPressedKey(e)}
onClick={(e) => changeCourseSelection()} >
<Card.Img variant="top" src={courseCoverUrl} className="course-card-image" title={props.course.book_title}/>
<Card.Img variant="top" src={courseCoverUrl} className="course-card-image" title={courseCoverTitle} />
<Card.Body>
<Card.Title className="course-card-title">{props.course.book_title ? props.course.book_title : 'This course does not have a title.'}</Card.Title>
<Card.Title className="course-card-title">{courseTitle}</Card.Title>
<Card.Subtitle className="course-card-subtitle">Released: {props.course.release_date ? formatDate(props.course.release_date) : 'No release date has been provided.'}</Card.Subtitle>
</Card.Body>
</Card>
Expand Down
5 changes: 4 additions & 1 deletion src/main/frontend/src/features/CoursePicker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Redux imports
import { useSelector } from 'react-redux';
import { selectErrorAssociatingCourse, selectErrorFetchingCourses } from '../app/appSlice';
import {
selectErrorAssociatingCourse,
selectErrorFetchingCourses
} from '../app/appSlice';

// Components import
import Col from 'react-bootstrap/Col';
Expand Down
26 changes: 16 additions & 10 deletions src/main/frontend/src/features/CoursePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
selectRootOutcomeGuid,
selectSelectedModules,
selectState,
selectTarget
selectTarget,
setIsFetchingDeepLinks
} from '../app/appSlice';

import { parseCourseCoverImage } from '../util/Utils.js';
Expand Down Expand Up @@ -47,6 +48,8 @@ function CoursePreview(props) {

// Some courses may not have a valid cover image, use a default instead
const courseCoverUrl = parseCourseCoverImage(props.course.cover_img_url, true);
const courseTitle = props.course.book_title ? props.course.book_title : 'This course does not have a title.';
const courseCoverTitle = `The cover image for the ${courseTitle} course.`;
const dispatch = useDispatch();

// In case there's an error adding the links, display an error message.
Expand All @@ -60,6 +63,8 @@ function CoursePreview(props) {
const [selectAllChecked, setSelectAllChecked] = useState(!isReturningUser);
// If the course has been paired with a Lumen course we must display a different text for the button.
const addButtonText = !isReturningUser ? 'Add Course' : 'Add Content';
const helpText = !isReturningUser ? 'Clicking Add Course will add the selected content for this Lumen course to your LMS' :
'Clicking Add Content will add the selected content for this Lumen course to your LMS';

// When the user clicks cancel it should reset the module and the course selection.
const resetSelection = () => {
Expand All @@ -76,8 +81,13 @@ function CoursePreview(props) {

const addCourseToLMS = () => {

// The window will never notice if the user is browsing in long contents or not, should always scroll to top when fetching items.
window.scrollTo(0, 0);

// Display the spinner when fetching the deep links.
setFetchingDeepLinks(true);
// Notify other components that a deep linking request has been started.
dispatch(setIsFetchingDeepLinks(true));

const contextAPIUrl = target.replace("/lti3", "/context");

Expand All @@ -93,7 +103,6 @@ function CoursePreview(props) {
}

// TODO: Handle error cases of blank/null values

const bookPairingData = {
"id_token": idToken,
"root_outcome_guid": props.course.root_outcome_guid,
Expand Down Expand Up @@ -122,13 +131,10 @@ function CoursePreview(props) {
form.submit();
}).catch(reason => {
setErrorAddingLinks(true);
}).finally( () => {

// The window will never notice if the user is browsing in long contents or not, should always scroll to top when navigating across courses.
window.scrollTo(0, 0);

// Remove the spinner once the request has responded.
// Remove the spinner once the request has responded with an error, otherwise the LMS will close the modal.
setFetchingDeepLinks(false);
// Notify other components that the request has been completed with an error.
dispatch(setIsFetchingDeepLinks(false));
});
}

Expand All @@ -153,7 +159,7 @@ function CoursePreview(props) {
<div className="course-info">
<Row className="course-preview-info">
<Col sm={2}>
<Image rounded fluid src={courseCoverUrl} title={props.course.book_title} />
<Image rounded fluid src={courseCoverUrl} title={courseCoverTitle} />
</Col>
<Col sm={10}>
{(props.course.book_title || props.course.description) ?
Expand All @@ -169,7 +175,7 @@ function CoursePreview(props) {
<CourseTOC topics={props.course.table_of_contents} />
</div>
<div className="fixed-bottom course-footer d-flex flex-row">
<p className="action-info">Clicking Add Course will add all of the content for this Lumen course to your LMS</p>
<p className="action-info">{helpText}</p>
<div className="ms-auto mx-3 d-flex d-row">
{!isReturningUser && <Button variant="secondary" onClick={(e) => resetSelection()}>Cancel</Button>}
<Button disabled={!hasSelectedModules} variant="primary" className="ms-1" onClick={(e) => addCourseToLMS()}>{addButtonText}</Button>
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/src/features/CourseTopic.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function CourseTopic (props) {
if (Array.isArray(props.topic.sub_topics) && props.topic.sub_topics.length) {
courseSectionSubtopics = (<ul>
{props.topic.sub_topics.map((item, index) => {
return <li key={index}>{item}</li>
return <li key={index} tabIndex="0">{item}</li>
})}
</ul>);
}
Expand Down
22 changes: 22 additions & 0 deletions src/main/frontend/src/features/LoadingCoursesPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Component imports
import Container from 'react-bootstrap/Container';
import Spinner from 'react-bootstrap/Spinner';

function LoadingCoursesPage (props) {

// Display a Spinner when courses are being loaded.
const loadingText = 'Loading content, please wait...';
const loadingComponent = <div className="loading-courses d-flex justify-content-center align-items-center m-4">
<Spinner animation="border" role="status">
<span className="visually-hidden">{loadingText}</span>
</Spinner>
{loadingText}
</div>;

return <Container className="App" fluid role="main">
{loadingComponent}
</Container>;

}

export default LoadingCoursesPage;
5 changes: 4 additions & 1 deletion src/main/frontend/src/features/LtiBreadcrumb.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
changeSelectedCourse,
selectSelectedCourse,
selectRootOutcomeGuid,
selectIsFetchingDeepLinks,
toggleAllModules
} from '../app/appSlice';
// Component imports
Expand All @@ -15,6 +16,8 @@ function LtiBreadcrumb(props) {
const dispatch = useDispatch();
const selectedCourse = useSelector(selectSelectedCourse);
const rootOutcomeGuid = useSelector(selectRootOutcomeGuid);
// When the links are being added to the course we should not display the breadcrumbs.
const isFetchingDeepLinks = useSelector(selectIsFetchingDeepLinks);

// A returning user means a user that previously associated a course with the LMS course, exists a previous root_outcome_guid selection.
// When a course has been paired with the LMS, do not display the breadcrumb so the user cannot go back and select a different course.
Expand All @@ -27,7 +30,7 @@ function LtiBreadcrumb(props) {

return (
<Breadcrumb role="navigation">
{!isReturningUser &&
{ (!isFetchingDeepLinks && !isReturningUser ) &&
<>
<Breadcrumb.Item onClick={(e) => resetSelection()}>Lumen Learning</Breadcrumb.Item>
<Breadcrumb.Item active={!selectedCourse} onClick={(e) => resetSelection()}>Add Course</Breadcrumb.Item>
Expand Down
3 changes: 3 additions & 0 deletions src/main/frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const initialState = {
searchInputText: '',
selectedCourse: null,
loading: true,
isFetchingDeepLinks: false,
errorFetchingCourses: false,
errorAssociatingCourse: false,
selectedModules: [],
Expand All @@ -43,6 +44,8 @@ const initialState = {
state: ltiLaunchData.state,
target: ltiLaunchData.target,
root_outcome_guid: isValidRootOutcomeGuid(ltiLaunchData.root_outcome_guid) ? ltiLaunchData.root_outcome_guid : null,
platform_family_code: ltiLaunchData.platform_family_code,
lti_system_error: ltiLaunchData.lti_system_error,
};

// Creates the store and preloads the initial state of the store.
Expand Down
6 changes: 6 additions & 0 deletions src/main/frontend/src/util/LtiSystemErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// The error codes come from the backend, reflects situations where the user should be informed.
export const LTI_SYSTEM_ERRORS = {
LINEITEMS_SYNCING_ERROR: 0,
DYNAMIC_REGISTRATION_DUPLICATE_ERROR: 1,
DYNAMIC_REGISTRATION_GENERAL_ERROR: 2
}
4 changes: 4 additions & 0 deletions src/main/java/net/unicon/lti/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
"classpath:/static/static/js/",
"classpath:/static/static/css/"
);

registry.addResourceHandler("/index.html")
.addResourceLocations("classpath:/static/index.html")
.setCachePeriod(0);
}

}
1 change: 1 addition & 0 deletions src/main/java/net/unicon/lti/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/ags/**")
.antMatchers("/demo/**")
.antMatchers("/harmony/**")
.antMatchers("/valkyrie/**")
.and()
.authorizeRequests().anyRequest().permitAll().and().csrf().disable().headers().frameOptions().disable();
}
Expand Down
Loading

0 comments on commit c28bf44

Please sign in to comment.