diff --git a/actions/slide/addLTI.js b/actions/slide/addLTI.js
new file mode 100644
index 000000000..c3f8056db
--- /dev/null
+++ b/actions/slide/addLTI.js
@@ -0,0 +1,33 @@
+import UserProfileStore from '../../stores/UserProfileStore';
+import {shortTitle} from '../../configs/general';
+import striptags from 'striptags';
+import TreeUtil from '../../components/Deck/TreePanel/util/TreeUtil';
+const log = require('../log/clog');
+import addActivity from '../activityfeed/addActivity';
+import {navigateAction} from 'fluxible-router';
+import Util from '../../components/common/Util';
+
+export default function addLTI(context, payload, done) {
+ //log.info(context);
+ //console.log('actions/slide/addLTI.js');
+ //enrich with user id
+ let userid = context.getStore(UserProfileStore).userid;
+ if (userid != null && userid !== '') {
+
+ context.service.create('lticonsumer', payload, {timeout: 20 * 1000}, (err, res) => {
+ //console.log('addLTI.js');
+ //console.log('res='+res);
+ if (err) {
+ console.log(err);
+ //console.log('ADD_LTI_FAILURE');
+ context.dispatch('ADD_LTI_FAILURE', err);
+ } else {
+ context.dispatch('ADD_LTI_SUCCESS', res);
+ }
+ done();
+ });
+
+ } //end if (userid != null && userid !== '')
+ else
+ done();
+}
diff --git a/actions/user/userprofile/cancelUserlti.js b/actions/user/userprofile/cancelUserlti.js
new file mode 100644
index 000000000..9403e983c
--- /dev/null
+++ b/actions/user/userprofile/cancelUserlti.js
@@ -0,0 +1,11 @@
+import UserProfileStore from '../../../stores/UserProfileStore';
+import {navigateAction} from 'fluxible-router';
+
+export default function cancelUserlti(context, payload, done) {
+
+ context.dispatch('CANCEL_USERLTI_SUCCESS');
+ context.executeAction(navigateAction, {
+ url: '/user/' + context.getStore(UserProfileStore).username + '/ltis/overview'
+ });
+
+}
diff --git a/actions/user/userprofile/chooseAction.js b/actions/user/userprofile/chooseAction.js
index 73aa818af..d940d6fe7 100644
--- a/actions/user/userprofile/chooseAction.js
+++ b/actions/user/userprofile/chooseAction.js
@@ -5,13 +5,16 @@ import notFoundError from '../../error/notFoundError';
const log = require('../../log/clog');
import loadUserCollections from '../../collections/loadUserCollections';
import loadUserRecommendations from '../../recommendations/loadUserRecommendations';
-import { shortTitle } from '../../../configs/general';
+import { shortTitle, LTI_ID } from '../../../configs/general';
+import UserProfileStore from '../../../stores/UserProfileStore';
+
import loadUserStats from '../../stats/loadUserStats';
export const categories = { //Do NOT alter the order of these items! Just add your items. Used in UserProfile and CategoryBox components
- categories: ['settings', 'groups', 'playlists', 'decks', 'recommendations', 'stats'],
+ categories: ['settings', 'groups', 'playlists', 'decks', 'recommendations', 'stats', 'ltis'],
settings: ['profile', 'account', 'integrations'],
groups: ['overview'],
+ ltis: ['overview', 'edit'],
decks: ['shared'],
};
@@ -19,6 +22,7 @@ export function chooseAction(context, payload, done) {
log.info(context);
let title = shortTitle + ' | ';
+
switch(payload.params.category){
case categories.categories[0]:
switch(payload.params.item){
@@ -29,7 +33,7 @@ export function chooseAction(context, payload, done) {
title += 'Account';
break;
case categories.settings[2]:
- title += 'Authorized Accounts';
+ title += 'Authorized Accounts & Services';
break;
default:
title = shortTitle;
@@ -63,6 +67,23 @@ export function chooseAction(context, payload, done) {
case categories.categories[5]:
title += 'User Stats';
break;
+ /*
+ case categories.categories[6]:
+ switch(payload.params.item){
+ case categories.ltis[0]:
+ title += 'My Learning Services';
+ break;
+ case categories.ltis[1]:
+ title += 'Add Learning Service';
+ break;
+ default:
+ title = shortTitle;
+ break;
+ };
+ break;
+ */
+
+
default:
title = shortTitle;
};
@@ -73,6 +94,7 @@ export function chooseAction(context, payload, done) {
context.executeAction(fetchUser, payload, callback);
},
(callback) => {
+
switch (payload.params.category) {
case categories.categories[0]:
case categories.categories[1]:
@@ -103,9 +125,18 @@ export function chooseAction(context, payload, done) {
context.dispatch('USER_CATEGORY', {category: payload.params.category});
context.executeAction(loadUserStats, {}, callback);
break;
+ case categories.categories[6]:
+ if(!categories.settings.includes(payload.params.item) && !categories.ltis.includes(payload.params.item) ){
+ context.executeAction(notFoundError, {}, callback);
+ break;
+ }
+ context.dispatch('USER_CATEGORY', {category: payload.params.category, item: payload.params.item});
+ callback();
+ break;
default:
context.executeAction(notFoundError, {}, callback);
}
+
}
],
(err, result) => {
diff --git a/actions/user/userprofile/deleteUserlti.js b/actions/user/userprofile/deleteUserlti.js
new file mode 100644
index 000000000..2418198bf
--- /dev/null
+++ b/actions/user/userprofile/deleteUserlti.js
@@ -0,0 +1,14 @@
+import UserProfileStore from '../../../stores/UserProfileStore';
+
+export default function deleteUserlti(context, payload, done) {
+ context.dispatch('UPDATE_USERLTIS_STATUS', null);
+ payload.jwt = context.getStore(UserProfileStore).jwt;
+ context.service.update('userProfile.deleteUserlti', payload, { timeout: 20 * 1000 }, (err, res) => {
+ if (err) {
+ context.dispatch('DELETE_USERLTI_FAILED', err);
+ } else {
+ context.dispatch('DELETE_USERLTI_SUCCESS', payload.ltiid);
+ }
+ done();
+ });
+}
diff --git a/actions/user/userprofile/leaveUserlti.js b/actions/user/userprofile/leaveUserlti.js
new file mode 100644
index 000000000..720a8450e
--- /dev/null
+++ b/actions/user/userprofile/leaveUserlti.js
@@ -0,0 +1,14 @@
+import UserProfileStore from '../../../stores/UserProfileStore';
+
+export default function leaveUserlti(context, payload, done) {
+ context.dispatch('UPDATE_USERLTIS_STATUS', null);
+ payload.jwt = context.getStore(UserProfileStore).jwt;
+ context.service.update('userProfile.leaveUserlti', payload, { timeout: 20 * 1000 }, (err, res) => {
+ if (err) {
+ context.dispatch('LEAVE_USERLTI_FAILED', err);
+ } else {
+ context.dispatch('LEAVE_USERLTI_SUCCESS', payload.ltiid);
+ }
+ done();
+ });
+}
diff --git a/actions/user/userprofile/saveUserlti.js b/actions/user/userprofile/saveUserlti.js
new file mode 100644
index 000000000..cf1cec2ca
--- /dev/null
+++ b/actions/user/userprofile/saveUserlti.js
@@ -0,0 +1,18 @@
+import UserProfileStore from '../../../stores/UserProfileStore';
+import {navigateAction} from 'fluxible-router';
+
+export default function saveUserlti(context, payload, done) {
+ context.dispatch('SAVE_USERLTI_START', {});
+ payload.jwt = context.getStore(UserProfileStore).jwt;
+ context.service.update('userProfile.saveUserlti', payload, {timeout: 60 * 1000}, { timeout: 60 * 1000 }, (err, res) => {
+ if (err) {
+ context.dispatch('SAVE_USERLTI_FAILED', err);
+ } else {
+ context.dispatch('SAVE_USERLTI_SUCCESS', res);
+ context.executeAction(navigateAction, {
+ url: '/user/' + context.getStore(UserProfileStore).username + '/ltis/overview'
+ });
+ }
+ done();
+ });
+}
diff --git a/actions/user/userprofile/updateUserlti.js b/actions/user/userprofile/updateUserlti.js
new file mode 100644
index 000000000..1740d0d29
--- /dev/null
+++ b/actions/user/userprofile/updateUserlti.js
@@ -0,0 +1,27 @@
+const log = require('../../log/clog');
+import { shortTitle } from '../../../configs/general';
+
+export default function updateUserlti(context, payload, done) {
+ log.info(context);
+
+ if (payload.offline) {
+ context.dispatch('UPDATE_USERLTI', payload.lti);
+ context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: shortTitle + ' | Edit LTI ' + payload.lti.name});
+ return done();
+ }
+
+ let payload2 = {
+ ltiid: payload.lti._id
+ };
+
+ context.service.read('userlti.read', payload2, { timeout: 20 * 1000 }, (err, res) => {
+ if (err) {
+ context.dispatch('UPDATE_USERLTI', payload.lti);
+ }
+ else {
+ context.dispatch('UPDATE_USERLTI', res[0]);
+ context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: shortTitle + ' | Edit LTI ' + res[0].name});
+ }
+ done();
+ });
+}
diff --git a/components/Deck/ContentPanel/ContentActions/DownloadModal.js b/components/Deck/ContentPanel/ContentActions/DownloadModal.js
index cec350702..c6dcbd2e2 100644
--- a/components/Deck/ContentPanel/ContentActions/DownloadModal.js
+++ b/components/Deck/ContentPanel/ContentActions/DownloadModal.js
@@ -104,6 +104,12 @@ class DownloadModal extends React.Component{
case 'HTML':
return Microservices.pdf.uri +'/exportOfflineHTML/'+ splittedId[0];
break;
+ case 'xAPI Launch (Live)':
+ return Microservices.xapi.uri +'/getTinCanPackage/' + splittedId[0]+ '?offline=false&format=xml';
+ break;
+ case 'xAPI Launch (Offline)':
+ return Microservices.xapi.uri +'/getTinCanPackage/' + splittedId[0]+ '?offline=true&format=xml';
+ break;
case 'SCORMv1.2':
case 'SCORMv2':
case 'SCORMv3':
@@ -111,6 +117,8 @@ class DownloadModal extends React.Component{
let version = type.split('v'); //separates format from version. In second position we have the version
return Microservices.pdf.uri + '/exportSCORM/' + splittedId[0]+ '?version='+version[1];
break;
+
+
default:
return '';
@@ -246,6 +254,37 @@ class DownloadModal extends React.Component{
/>
+
+
+
+
+
+
+
+
+
';
+ }
+ else if(nextProps.SlideEditStore.ltiResponseHTML !== '') {
+ let newHTML = nextProps.SlideEditStore.ltiResponseHTML.replace(/\"/g, '\'');
+ iframe = '';
+ }
+ if($('.pptx2html').length) //if slide is in canvas mode
+ {
+ $('.pptx2html').append(''+iframe+'
');
+ this.hasChanges = true;
+ //this.correctDimensionsBoxes('iframe');
+ }
+ else { //if slide is in non-canvas mode
+ this.refs.inlineContent.innerHTML += iframe;
+ }
+ if($('.pptx2html').length) //if slide is in canvas mode
+ {
+ //this.uniqueIDAllElements();
+ this.resizeDrag();
+ }
+ }
+
if (nextProps.SlideEditStore.title !== '' &&
nextProps.SlideEditStore.title !== this.props.SlideEditStore.title &&
nextProps.SlideEditStore.LeftPanelTitleChange !== false)
diff --git a/components/Deck/SlideEditLeftPanel/SlideEditLeftPanel.js b/components/Deck/SlideEditLeftPanel/SlideEditLeftPanel.js
index 20bb477dd..aa9da0bd4 100644
--- a/components/Deck/SlideEditLeftPanel/SlideEditLeftPanel.js
+++ b/components/Deck/SlideEditLeftPanel/SlideEditLeftPanel.js
@@ -12,6 +12,7 @@ import mathsClick from '../../../actions/slide/mathsClick';
import codeClick from '../../../actions/slide/codeClick';
import removeBackgroundClick from '../../../actions/slide/removeBackgroundClick';
import embedClick from '../../../actions/slide/embedClick';
+import addLTI from '../../../actions/slide/addLTI';
import changeTemplate from '../../../actions/slide/changeTemplate';
import HTMLEditorClick from '../../../actions/slide/HTMLEditorClick';
import AttachQuestions from '../ContentPanel/AttachQuestions/AttachQuestionsModal';
@@ -46,6 +47,17 @@ class SlideEditLeftPanel extends React.Component {
showSize: false,
showTransition: false,
showBackground: false,
+
+ showLTI: false,
+ ltiURL: '',
+ ltiKey: '',
+ ltiWidth: '400',
+ ltiHeight: '300',
+ ltiResponseURL: '',
+ ltiResponseHTML: '',
+ ltiURLMissingError: false,
+ ltiKeyMissingError: false,
+
slideTitle: this.props.SlideEditStore.title,
deckID: this.props.DeckPageStore.selector.id,
slideSizeText: '',
@@ -74,6 +86,7 @@ class SlideEditLeftPanel extends React.Component {
if (prevState.showTemplate !== this.state.showTemplate ||
prevState.showOther !== this.state.showOther ||
prevState.showEmbed !== this.state.showEmbed ||
+ prevState.showLTI !== this.state.showLTI ||
prevState.showProperties !== this.state.showProperties ||
prevState.showTitleChange !== this.state.showTitleChange ||
prevState.showSize !== this.state.showSize ||
@@ -84,6 +97,9 @@ class SlideEditLeftPanel extends React.Component {
if (this.state.showTitleChange === true)
{
$('#slideTitle').focus();
+ } else if (this.state.showLTI === true)
+ {
+ $('#ltiKey').focus();
} else if (this.state.showEmbed === true)
{
$('#embedTitle').focus();
@@ -196,6 +212,64 @@ class SlideEditLeftPanel extends React.Component {
this.setState({showEmbed: false});
}
}
+
+ //LTI handles
+ handleLTIClick(){
+ console.log('handleLTIClick');
+ this.setState({showLTI: true});
+ this.setState({showOther: false});
+ this.forceUpdate();
+ }
+ handleLTIAddClick(){
+ //console.log('handleLTIAddClick');
+ if(this.state.ltiURL === '' || this.state.ltiKey === ''){
+ this.setState({ ltiURLMissingError: true });
+ this.setState({ ltiKeyMissingError: true });
+ //console.log('errormissing');
+ this.forceUpdate();
+ }
+ else {
+ //console.log('post request');
+ let oauth = require('oauth-sign');
+ let btoa = require('btoa');
+ let timestamp = Math.round(Date.now() / 1000);
+ let method = 'POST';
+
+ let ltiURL = this.state.ltiURL;
+ let key = this.state.ltiKey;
+ let secret = this.state.ltiKey;
+
+ let params = {
+ lti_message_type: 'basic-lti-launch-request',
+ lti_version: 'LTI-1p0',
+ resource_link_id: 'resourceLinkId',
+ oauth_consumer_key: key,
+ oauth_nonce: btoa(timestamp),
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: timestamp,
+ oauth_version: '1.0',
+ ext_user_username: 'slidewiki'
+ };
+
+ let signature = oauth.hmacsign(method, ltiURL, params, secret);
+ params.oauth_signature = signature;
+ //console.log("params.oauth_signature="+params.oauth_signature);
+ this.context.executeAction(addLTI, {
+ ltiURL: ltiURL,
+ ltiKey: key,
+ ltiWidth : this.state.ltiWidth,
+ ltiHeight : this.state.ltiHeight,
+ params: params
+
+ });
+ }//end else
+ }
+ handleBackLTI(){
+ this.setState({showOther: true});
+ this.setState({showLTI: false});
+ this.forceUpdate();
+ }
+
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
@@ -448,6 +522,15 @@ class SlideEditLeftPanel extends React.Component {
case 'handleHelpClick':
this.handleHelpClick();
break;
+ case 'handleLTIClick':
+ this.handleLTIClick();
+ break;
+ case 'handleEmbedAddClick':
+ this.handleEmbedAddClick();
+ break;
+ case 'handleLTIAddClick':
+ this.handleLTIAddClick();
+ break;
case 'handleChangeBackgroundColorClick':
this.handleChangeBackgroundColorClick();
break;
@@ -496,6 +579,9 @@ class SlideEditLeftPanel extends React.Component {
this.handleKeyPress(evt, 'handleEmbedClick')}>
+ this.handleKeyPress(evt, 'handleLTIClick')}>
+
+
this.handleKeyPress(evt, 'handleTableClick')}>
@@ -569,6 +655,53 @@ class SlideEditLeftPanel extends React.Component {
);
+ let ltiOptions = (
+ );
+
const templateListStyle = {
maxHeight: 600,
minHeight: 320,
@@ -820,6 +953,8 @@ class SlideEditLeftPanel extends React.Component {
panelcontent = otherList;
} else if (this.state.showEmbed) {
panelcontent = embedOptions;
+ } else if (this.state.showLTI) {
+ panelcontent = ltiOptions;
} else if (this.state.showProperties){
panelcontent = propertiesContent;
} else if (this.state.showTitleChange){
diff --git a/components/Login/LTI.js b/components/Login/LTI.js
new file mode 100644
index 000000000..c4e098a84
--- /dev/null
+++ b/components/Login/LTI.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import {handleRoute} from 'fluxible-router';
+import {connectToStores} from 'fluxible-addons-react';
+import {navigateAction} from 'fluxible-router';
+import ReactDOM from 'react-dom';
+import cookie from 'react-cookie';
+let classNames = require('classnames');
+
+const NAME = 'ltilogin_data';
+
+let queryData = '';
+let resource_id= '';
+
+class LTI extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.componentDidMount = this.componentDidMount.bind(this);
+ this.state = {
+ queryData: this.props.currentRoute.query.data,
+ resource_id: this.props.currentRoute.query.resource_id
+ };
+ cookie.save('user_json_storage', this.state.queryData, {path: '/'});
+ }
+
+ componentWillMount() {
+ console.log('Will be called on the server...');
+ }
+ componentDidMount() {
+ console.log('LTI. componentDidMount');
+ //console.log('LTI. componentDidMount.this.state.resource_Id='+this.state.resource_id);
+ if(this.state.resource_id !== '' && this.state.resource_id >0) {
+ this.context.executeAction(navigateAction, {
+ url: '/deck/'+this.state.resource_id
+ });
+ }
+
+ }
+
+ render() {
+ //console.log('LTILogin.render called.resource_id='+this.state.resource_id);
+ return (
+
+ Welcome to LTI Login.
+
+ );
+ }
+}
+
+
+LTI.contextTypes = {
+ executeAction: React.PropTypes.func.isRequired
+};
+LTI = handleRoute(LTI);
+export default LTI;
diff --git a/components/User/UserProfile/CategoryBox.js b/components/User/UserProfile/CategoryBox.js
index 889f16ea9..241fa2b56 100644
--- a/components/User/UserProfile/CategoryBox.js
+++ b/components/User/UserProfile/CategoryBox.js
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { NavLink } from 'fluxible-router';
import { FormattedMessage, defineMessages } from 'react-intl';
+import { LTI_ID } from '../../../configs/general';
class CategoryBox extends React.Component {
constructor(props){
@@ -11,6 +12,7 @@ class CategoryBox extends React.Component {
}
render() {
+ //console.log('CategoryBox.props.username='+this.props.username);
return (
@@ -37,7 +39,7 @@ class CategoryBox extends React.Component {
@@ -46,7 +48,7 @@ class CategoryBox extends React.Component {
diff --git a/components/User/UserProfile/ChangePersonalData.js b/components/User/UserProfile/ChangePersonalData.js
index d8fa09f49..d91e45537 100644
--- a/components/User/UserProfile/ChangePersonalData.js
+++ b/components/User/UserProfile/ChangePersonalData.js
@@ -9,6 +9,7 @@ import {getLanguageName, getLanguageNativeName} from '../../../common';
import { writeCookie } from '../../../common';
import IntlStore from '../../../stores/IntlStore';
import { locales, flagForLocale }from '../../../configs/locales';
+import { LTI_ID } from '../../../configs/general';
import { Dropdown, Label } from 'semantic-ui-react';
@@ -75,7 +76,12 @@ class ChangePersonalData extends React.Component {
let emailToolTipp = this.props.failures.emailNotAllowed ? this.context.intl.formatMessage(messages.emailNotAllowed) : undefined;
let languageOptions = this.getLocaleOptions();
let currentLocale = (this.state.currentLocale.length <= 2) ? this.state.currentLocale : 'en';
- return (
+
+ //console.log("ChangePersonalData.props.username="+this.props.user.uname);
+ //console.log("LTI_ID="+LTI_ID);
+ if (!this.props.user.uname.endsWith(LTI_ID))
+ {
+ return (
+
+
+
+
+
+
+
+
+
);
}
@@ -436,4 +471,10 @@ Integrations.contextTypes = {
intl: PropTypes.object.isRequired
};
+Integrations = connectToStores(Integrations, [UserProfileStore], (context, props) => {
+ return {
+ UserProfileStore: context.getStore(UserProfileStore).getState()
+ };
+});
+
export default Integrations;
diff --git a/components/User/UserProfile/UserLTIEdit.js b/components/User/UserProfile/UserLTIEdit.js
new file mode 100644
index 000000000..5185bc125
--- /dev/null
+++ b/components/User/UserProfile/UserLTIEdit.js
@@ -0,0 +1,271 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Microservices } from '../../../configs/microservices';
+import {NavLink, navigateAction} from 'fluxible-router';
+import { TextArea } from 'semantic-ui-react';
+import { timeSince } from '../../../common';
+import UserPicture from '../../common/UserPicture';
+import updateUserlti from '../../../actions/user/userprofile/updateUserlti';
+import saveUserlti from '../../../actions/user/userprofile/saveUserlti';
+import cancelUserlti from '../../../actions/user/userprofile/cancelUserlti';
+
+class UserLTIEdit extends React.Component {
+ constructor(props){
+ super(props);
+
+ this.styles = {'backgroundColor': '#2185D0', 'color': 'white'};
+ }
+
+ componentDidUpdate() {
+ // console.log('UserLTIEdit componentDidUpdate:', this.props.saveUserLTIError, this.props.currentUserlti);
+ if (this.props.saveUserltiError) {
+ swal({
+ title: 'Error',
+ text: 'Unknown error while saving.',
+ type: 'error',
+ confirmButtonText: 'Close',
+ confirmButtonClass: 'negative ui button',
+ allowEscapeKey: true,
+ allowOutsideClick: true,
+ buttonsStyling: false
+ })
+ .then(() => {
+ return true;
+ })
+ .catch();
+ return;
+ }
+ this.refs.LTIKey.value = this.props.currentUserlti.key || '';
+ this.refs.LTISecret.value = this.props.currentUserlti.secret || '';
+ }
+
+ componentDidMount() {
+ $('#userlti_edit_dropdown_usernames_remote')
+ .dropdown({
+ apiSettings: {
+ url: Microservices.user.uri + '/information/username/search/{query}',
+ cache: false
+ },
+ saveRemoteData: false,
+ action: (name, value, source) => {
+ // console.log('dropdown select', name, value, source);
+
+ $('#userlti_edit_dropdown_usernames_remote').dropdown('clear');
+ $('#userlti_edit_dropdown_usernames_remote').dropdown('hide');
+
+ let lti = this.getLTI(this.props.currentUserlti.members);
+ if (lti.members === undefined || lti.members === null)
+ lti.members = [];
+
+ let data = JSON.parse(decodeURIComponent(value));
+ console.log('trying to add', name, 'to', lti.members, ' with ', data);
+ if (lti.members.findIndex((member) => {
+ return member.userid === parseInt(data.userid);
+ }) === -1 && data.username !== this.props.username) {
+ lti.members.push({
+ username: data.username,
+ userid: parseInt(data.userid),
+ joined: data.joined || undefined,
+ picture: data.picture,
+ country: data.country,
+ organization: data.organization
+ });
+ }
+
+ this.context.executeAction(updateUserlti, {lti: lti, offline: true});
+
+ return true;
+ }
+ });
+ }
+
+ getLTI(members = undefined) {
+
+ let lti = {
+ _id: this.props.currentUserlti._id,
+ key: this.refs.LTIKey.value,
+ secret: this.refs.LTISecret.value,
+ members: members,
+ timestamp: this.props.currentUserlti.timestamp || '',
+ creator: this.props.currentUserlti.creator || this.props.userid
+ };
+
+ if (this.props.currentUserlti._id)
+ lti.id = lti._id;
+
+ //TODO get members from list
+
+ return lti;
+ }
+
+ handleClickOnEditLTI(e) {
+ e.preventDefault();
+ }
+
+ handleSave(e) {
+ e.preventDefault();
+
+ let lti = this.getLTI(this.props.currentUserlti.members);
+
+ console.log('handleSave:', lti);
+
+ if (lti.key === '') {
+ swal({
+ title: 'Error',
+ text: 'At least you have to specify the LTI Key.',
+ type: 'error',
+ confirmButtonText: 'Close',
+ confirmButtonClass: 'negative ui button',
+ allowEscapeKey: true,
+ allowOutsideClick: true,
+ buttonsStyling: false
+ })
+ .then(() => {
+ return true;
+ })
+ .catch();
+ return;
+ }
+
+ this.context.executeAction(saveUserlti, lti);
+ }
+
+ handleCancel(e) {
+ e.preventDefault();
+ this.context.executeAction(cancelUserlti);
+ }
+
+ handleClickRemoveMember(member) {
+ // console.log('handleClickRemoveMember', member, 'from', this.props.currentUserlti.members);
+ let lti = this.getLTI(this.props.currentUserlti.members);
+
+ lti.members = lti.members.filter((gmember) => {
+ return gmember.userid !== member.userid;
+ });
+
+ this.context.executeAction(updateUserlti, {lti: lti, offline: true});
+ }
+
+ render() {
+ //console.log('UserLTIEdit rendered');
+ const signUpLabelStyle = {width: '150px'};
+
+ let userlist = [];
+ //change header and data depending on lti should be created or edited
+ let header = 'Add Learning Service';
+ if (this.props.currentUserlti._id !== undefined) {
+ header = 'Edit Learning Service';
+ }
+
+ //add creator as default member
+ userlist.push(
+
+ );
+
+ // console.log('render UserLTIEdit:', this.props.currentUserlti.members);
+ if (this.props.currentUserlti.members !== undefined && this.props.currentUserlti.members.length > 0) {
+ this.props.currentUserlti.members.forEach((member) => {
+ let fct = () => {
+ this.handleClickRemoveMember(member);
+ };
+ let optionalElement = (member.organization || member.country) ? (
+
+ {member.organization || 'Unknown organization'} ({member.country || 'unknown country'})
+
+
+ ) : '';
+ let optionalText = (member.joined) ? ('Joined '+timeSince((new Date(member.joined)))+' ago') : '';
+ userlist.push(
+ (
+
+ )
+ );
+ });
+ }
+
+ return (
+
+
+
+
{header}
+
+
+
+
+
+
+
+
+
+
+
+
+ { (this.props.currentUserlti._id === undefined) ?
+
+ : ''
+ }
+
+
+
+ {(this.props.saveUserltiIsLoading === true) ?
: ''}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+UserLTIEdit.contextTypes = {
+ executeAction: PropTypes.func.isRequired
+};
+
+export default UserLTIEdit;
diff --git a/components/User/UserProfile/UserLTIs.js b/components/User/UserProfile/UserLTIs.js
new file mode 100644
index 000000000..e6d8ad3c2
--- /dev/null
+++ b/components/User/UserProfile/UserLTIs.js
@@ -0,0 +1,200 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {NavLink, navigateAction} from 'fluxible-router';
+import updateUserlti from '../../../actions/user/userprofile/updateUserlti';
+import deleteUserlti from '../../../actions/user/userprofile/deleteUserlti';
+import leaveUserlti from '../../../actions/user/userprofile/leaveUserlti';
+import { LTI_ID } from '../../../configs/general';
+
+class UserLTIs extends React.Component {
+ constructor(props){
+ super(props);
+
+ this.styles = {'backgroundColor': '#2185D0', 'color': 'white'};
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.error.action !== undefined && this.props.error === '') {
+ let message = 'Error while deleting the lti: ';
+ if (nextProps.error.action === 'leave')
+ message = 'Error while leaving the lti: ';
+ swal({
+ title: 'Error',
+ text: message + nextProps.error.message,
+ type: 'error',
+ confirmButtonText: 'Close',
+ confirmButtonClass: 'negative ui button',
+ allowEscapeKey: false,
+ allowOutsideClick: false,
+ buttonsStyling: false
+ })
+ .then(() => {
+ this.context.executeAction(updateUserlti, {lti: {}, offline: true});
+
+ return true;
+ })
+ .catch();
+ return;
+ }
+ }
+
+ handleClickOnEditLTI(e) {
+ e.preventDefault();
+ // console.log('handleClickOnEditLTI:', e.target.attributes.name.value);
+
+ const action = e.target.attributes.name.value; //eg. changeLTI_2
+ const ltiid = action.split('_')[1];
+
+ let lti = this.props.ltis.find((lti) => {
+ return lti._id.toString() === ltiid;
+ });
+
+ this.context.executeAction(updateUserlti, {lti: lti, offline: false});
+ this.context.executeAction(navigateAction, {
+ url: '/user/' + this.props.username + '/ltis/edit'
+ });
+ }
+
+ handleClickOnRemoveLTI(e) {
+ e.preventDefault();
+ console.log('handleClickOnRemoveLTI:', e.target.attributes.name.value);
+
+ const action = e.target.attributes.name.value; //eg. changeLTI_2
+ const ltiid = action.split('_')[1];
+
+ swal({
+ titleText: 'Are you sure you want to delete this service?',
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: 'Yes, delete it!'
+ }).then((accepted) => {
+ this.context.executeAction(deleteUserlti, {ltiid: ltiid});
+ swal('LTI Service successfully deleted');
+ }, (cancelled) => {/*do nothing*/})
+ .catch(swal.noop);
+ }
+
+ handleClickOnLeaveLTI(e) {
+ e.preventDefault();
+ console.log('handleClickOnLeaveLTI:', e.target.attributes.name.value);
+
+ const action = e.target.attributes.name.value; //eg. changeLTI_2
+ const ltiid = action.split('_')[1];
+
+ this.context.executeAction(leaveUserlti, {ltiid: ltiid});
+ }
+
+ handleCLickNewLTI(e) {
+ e.preventDefault();
+ this.context.executeAction(updateUserlti, {lti: {}, offline: true});
+ this.context.executeAction(navigateAction, {
+ url: '/user/' + this.props.username + '/ltis/edit'
+ });
+ }
+
+ render() {
+ let items = [];
+ console.log('render userLTIs:', this.props.userid, this.props.ltis);
+ if(! (this.props.username.endsWith(LTI_ID))){
+ this.props.ltis.forEach((lti) => {
+ items.push( (
+
+
+
+
+
{lti.key}
+
{lti.members.length+1} member{((lti.members.length+1) !== 1) ? 's': ''}
+
+
+
+ {((this.props.userid === lti.creator) || (this.props.userid === lti.creator.userid)) ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ ));
+ });
+ }//end if(! (this.props.username.endsWith(LTI_ID))
+ else{
+ this.props.ltis.forEach((lti) => {
+ items.push( (
+
+
+
+
+
{lti.key}
+
{lti.members.length+1} member{((lti.members.length+1) !== 1) ? 's': ''}
+
+
+
+ ));
+ });
+ }
+
+
+ if (this.props.ltis === undefined || this.props.ltis === null || this.props.ltis.length < 1) {
+ items = [(
+
+
+
No LTI Services connected.
+
+
+ )];
+ }
+
+ if(! (this.props.username.endsWith(LTI_ID))){
+ return (
+
+
+
Learning Services (LTIs)
+
+
+
+ {(this.props.status === 'pending') ?
: ''}
+
+ {items}
+
+ );
+ }//end if
+ else{
+ return (
+
+
+
Learning Services (LTIs)
+
+
+ {(this.props.status === 'pending') ?
: ''}
+
+ {items}
+
+ );
+ }//end else
+
+ }
+}
+
+UserLTIs.contextTypes = {
+ executeAction: PropTypes.func.isRequired
+};
+
+export default UserLTIs;
diff --git a/components/User/UserProfile/UserProfile.js b/components/User/UserProfile/UserProfile.js
index 003d3ca68..0921aa097 100644
--- a/components/User/UserProfile/UserProfile.js
+++ b/components/User/UserProfile/UserProfile.js
@@ -8,7 +8,9 @@ import DeactivateAccount from './DeactivateAccount';
import ChangePersonalData from './ChangePersonalData';
import IntlStore from '../../../stores/IntlStore';
import UserGroups from './UserGroups';
-import {connectToStores} from 'fluxible-addons-react';
+import UserLTIs from './UserLTIs';
+import UserLTIEdit from './UserLTIEdit';
+import { connectToStores } from 'fluxible-addons-react';
import UserProfileStore from '../../../stores/UserProfileStore';
import UserStatsStore from '../../../stores/UserStatsStore';
import UserGroupsStore from '../../../stores/UserGroupsStore';
@@ -109,6 +111,15 @@ class UserProfile extends React.Component {
return this.displayGroups();
break;
}});
+ case categories.categories[6]:
+ return this.addScaffold(() => {switch(this.props.UserProfileStore.categoryItem){
+ case categories.ltis[0]:
+ return this.displayLTIs();
+ break;
+ case categories.ltis[1]:
+ return this.displayLTIedit();
+ break;
+ }});
case 'stats':
return this.addScaffold(() => this.displayUserStats());
default:
@@ -243,6 +254,15 @@ class UserProfile extends React.Component {
return ();
}
+
+ displayLTIs() {
+ return ();
+ }
+
+ displayLTIedit() {
+ return ();
+ }
+
render() {
return (this.chooseView());
}
diff --git a/configs/general.js b/configs/general.js
index b3532b31a..fa7633bc3 100644
--- a/configs/general.js
+++ b/configs/general.js
@@ -12,4 +12,5 @@ export default {
publicRecaptchaKey: '6LdNLyYTAAAAAINDsVZRKG_E3l3Dvpp5sKboR1ET',
loglevel: 'debug',
ssoEnabled: true,
+ LTI_ID: '@lti.org',
};
diff --git a/configs/microservices.sample.js b/configs/microservices.sample.js
index e74ced470..877ba1f4e 100644
--- a/configs/microservices.sample.js
+++ b/configs/microservices.sample.js
@@ -93,6 +93,9 @@ export default {
'analytics': {
uri: 'https://analyticsservice.experimental.slidewiki.org'
},
+ 'xapi': {
+ uri: 'https://xapiservice.experimental.slidewiki.org'
+ },
'lrs': {
uri: 'https://api.learninglocker.experimental.slidewiki.org',
basicAuth :'MWEwNTkwMTg5M2Y4ZjIyZTY4ZThkMzhlYWE0NDZkZjAxZWUyNjdhODo2YjE5MzAxODhmZWM0OTg0ZjE1YzVhODI1Njg2NTY5NDk5YzRmODEz'
diff --git a/configs/routes.js b/configs/routes.js
index b7d4298e4..0ab12d7a9 100644
--- a/configs/routes.js
+++ b/configs/routes.js
@@ -799,6 +799,19 @@ export default {
done();
}
},
+ ltiLogin: {
+ path: '/ltiLogin',
+ method: 'get',
+ page: 'ltiLogin',
+ title: 'SlideWiki -- Login',
+ handler: require('../components/Login/LTI'),
+ action: (context, payload, done) => {
+ context.dispatch('UPDATE_PAGE_TITLE', {
+ pageTitle: shortTitle + ' | Login'
+ });
+ done();
+ }
+ },
deckfamily: {
path: '/deckfamily/:tag',
method: 'get',
diff --git a/intl/ca.json b/intl/ca.json
index dbc723800..cf82b47f9 100644
--- a/intl/ca.json
+++ b/intl/ca.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/default.json b/intl/default.json
index 6383d5954..87b6d47c9 100644
--- a/intl/default.json
+++ b/intl/default.json
@@ -645,6 +645,7 @@
"editpanel.slideSizeCurrent": "(current: {size})",
"editpanel.back": "back",
"editpanel.embed": "Embed",
+ "editpanel.lti": "LTI",
"editpanel.table": "Table",
"editpanel.Maths": "Maths",
"editpanel.Code": "Code",
@@ -661,6 +662,14 @@
"editpanel.embedAdd": "Add to Slide",
"editpanel.embedNote": "Not all website owners allow their content to be embedded. Using embed code provided by the website you want to embed (instead of URL) often works best.",
"editpanel.embedNoteTerms": "Please note that our terms (e.g., on malicious code and commercial material) also strictly apply to any content on webpages that you embed.",
+ "editpanel.ltiKey": "LTI Key:",
+ "editpanel.ltiKeyMissingError": "missing LTI key",
+ "editpanel.ltiURL": "URL/Link to LTI content:",
+ "editpanel.ltiURLMissingError": "missing URL/link to content",
+ "editpanel.ltiWidth": "Width of LTI content:",
+ "editpanel.ltiHeight": "Height of LTI content:",
+ "editpanel.ltiAdd": "Add to Slide",
+ "editpanel.ltiNote": "Use an LTI URL and key.",
"editpanel.template2": "Empty document - Document-mode (non-canvas)",
"editpanel.template3": "Document with title - Document-mode (non-canvas)",
"editpanel.template31": "Document with rich text example - Document-mode (non-canvas)",
@@ -1277,10 +1286,12 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
+ "CategoryBox.ltis": "Learning Services",
+ "CategoryBox.myLTIs": "My Learning Services",
"ChangePassword.passwordMismatch": "Your passwords do not match",
"ChangePassword.passwordToolTipp": "This is not the password you entered before - Please try again",
"ChangePassword.newPasswordTitle": "Your password should contain 8 characters or more",
@@ -1525,4 +1536,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/el.json b/intl/el.json
index 00efa9a5a..8ea1e010f 100644
--- a/intl/el.json
+++ b/intl/el.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Προσωπικές Ρυθμίσεις",
"CategoryBox.profile": "Προφίλ",
"CategoryBox.account": "Λογαριασμός",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Ομάδες",
"CategoryBox.myGroups": "Οι ομάδες μου",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/en.json b/intl/en.json
index ee35786b2..5b582f92f 100644
--- a/intl/en.json
+++ b/intl/en.json
@@ -474,6 +474,7 @@
"editpanel.slideSizeCurrent": "(current: {size})",
"editpanel.back": "back",
"editpanel.embed": "Embed",
+ "editpanel.lti": "LTI",
"editpanel.table": "Table",
"editpanel.Maths": "Maths",
"editpanel.Code": "Code",
@@ -490,6 +491,14 @@
"editpanel.embedAdd": "Add to Slide",
"editpanel.embedNote": "Not all website owners allow their content to be embedded. Using embed code provided by the website you want to embed (instead of URL) often works best.",
"editpanel.embedNoteTerms": "Please note that our terms (e.g., on malicious code and commercial material) also strictly apply to any content on webpages that you embed.",
+ "editpanel.ltiKey": "LTI Key:",
+ "editpanel.ltiKeyMissingError": "missing LTI key",
+ "editpanel.ltiURL": "URL/Link to LTI content:",
+ "editpanel.ltiURLMissingError": "missing URL/link to content",
+ "editpanel.ltiWidth": "Width of LTI content:",
+ "editpanel.ltiHeight": "Height of LTI content:",
+ "editpanel.ltiAdd": "Add to Slide",
+ "editpanel.ltiNote": "Use an LTI URL and key.",
"editpanel.template2": "Empty document - Document-mode (non-canvas)",
"editpanel.template3": "Document with title - Document-mode (non-canvas)",
"editpanel.template31": "Document with rich text example - Document-mode (non-canvas)",
@@ -1091,10 +1100,12 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
+ "CategoryBox.ltis": "Learning Services",
+ "CategoryBox.myLTIs": "My Learning Services",
"ChangePassword.passwordMismatch": "Your passwords do not match",
"ChangePassword.passwordToolTipp": "This is not the password you entered before - Please try again",
"ChangePassword.newPasswordTitle": "Your password should contain 8 characters or more",
@@ -1331,4 +1342,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/fy.json b/intl/fy.json
index 72af06824..91feea2a2 100644
--- a/intl/fy.json
+++ b/intl/fy.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/gd.json b/intl/gd.json
index 54b432314..1f531233b 100644
--- a/intl/gd.json
+++ b/intl/gd.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/pt.json b/intl/pt.json
index 855d76bee..cdc966bcf 100644
--- a/intl/pt.json
+++ b/intl/pt.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/intl/ru.json b/intl/ru.json
index 5802dccfc..8c1d1b522 100644
--- a/intl/ru.json
+++ b/intl/ru.json
@@ -1091,7 +1091,7 @@
"CategoryBox.personalSettings": "Personal settings",
"CategoryBox.profile": "Profile",
"CategoryBox.account": "Account",
- "CategoryBox.authorizedAccounts": "Authorized Accounts",
+ "CategoryBox.authorizedAccounts": "Authorized Accounts & Services",
"CategoryBox.userStats": "User Stats",
"CategoryBox.groups": "Groups",
"CategoryBox.myGroups": "My Groups",
@@ -1331,4 +1331,4 @@
"GroupMenu.settings": "Group Settings",
"GroupMenu.stats": "Group Stats",
"UserGroupPage.goBack": "Return to My Groups List"
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 6082dba52..f5e36b20a 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"babel-preset-stage-0": "^6.22.0",
"babel-register": "^6.26.0",
"body-parser": "^1.18.3",
+ "btoa": "^1.2.1",
"bundle-loader": "0.5.5",
"cheerio": "^0.22.0",
"ckeditor": "^4.6.2",
@@ -126,6 +127,7 @@
"mobile-detect": "^1.4.1",
"moment": "^2.22.2",
"napa": "^2.3.0",
+ "node-gyp": "^3.8.0",
"npm": "^5.5.1",
"pre-commit": "^1.2.2",
"prop-types": "^15.6.2",
diff --git a/server.js b/server.js
index a69b108a0..514da14e3 100644
--- a/server.js
+++ b/server.js
@@ -98,6 +98,8 @@ fetchrPlugin.registerService(require('./services/notifications'));
fetchrPlugin.registerService(require('./services/user'));
fetchrPlugin.registerService(require('./services/searchresults'));
fetchrPlugin.registerService(require('./services/usergroup'));
+fetchrPlugin.registerService(require('./services/userlti'));
+fetchrPlugin.registerService(require('./services/lticonsumer'));
fetchrPlugin.registerService(require('./services/userProfile'));
fetchrPlugin.registerService(require('./services/suggester'));
fetchrPlugin.registerService(require('./services/logservice'));
diff --git a/services/lticonsumer.js b/services/lticonsumer.js
new file mode 100644
index 000000000..d5f708110
--- /dev/null
+++ b/services/lticonsumer.js
@@ -0,0 +1,110 @@
+import {Microservices} from '../configs/microservices';
+import rp from 'request-promise';
+const log = require('../configs/log').log;
+
+export default {
+ name: 'lticonsumer',
+ // At least one of the CRUD methods is Required
+ /*
+ For now hardcoded slide template - powerpoint basic slide
+ */
+ create: (req, resource, params, body, config, callback) => {
+ //console.log("service/lticonsumer.js/resource="+resource);
+
+ req.reqId = req.reqId ? req.reqId : -1;
+ log.info({Id: req.reqId, Service: __filename.split('/').pop(), Resource: resource, Operation: 'create', Method: req.method});
+ //console.log('params='+params);
+ //console.log("params.ltiURL="+params.ltiURL);
+ let ltiURL = params.ltiURL;
+ let ltiKey = params.ltiKey;
+ let ltiHeight = params.ltiHeight;
+ let ltiWidth = params.ltiWidth;
+
+ // LTI paramters
+ let args = params.params? params.params : params;
+
+ let url = require('url');
+ let http = require('http');
+ let https = require('https');
+ let request = require('request');
+ let querystring = require('querystring');
+ let btoa = require('btoa');
+ let oauth = require('oauth-sign');
+ let fs = require('fs');
+
+ let parseURL = url.parse(ltiURL, true);
+ let hostname = parseURL.hostname;
+ let port = parseURL.port;
+ let pathname = parseURL.pathname;
+
+ //console.log('hostname='+hostname);
+ //console.log('path='+pathname);
+ //console.log('port='+port);
+
+ if(resource === 'lticonsumer'){
+ /*********connect to LTI Provider*************/
+ let post_data = querystring.stringify(args);
+ //console.log('lticonsumer.js.args='+JSON.stringify(args));
+ let request_options = {
+ hostname: hostname,
+ path: pathname,
+ port: port,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ };
+
+ let body = '';
+ let req = http.request(request_options, (res) => {
+ console.log('STATUS: ' + res.statusCode);
+ console.log('HEADERS: ' + JSON.stringify(res.headers));
+ let json = JSON.stringify(res.headers);
+ //console.log('res.headers.location: ' + res.headers.location);
+ res.setEncoding('utf8');
+ res.on('data', (chunk) => {
+ //console.log('BODY: ' + chunk);
+ body += chunk;
+ }).on('end', () => {
+ //console.log("body="+body);
+ //console.log("res.headers.location="+res.headers.location);
+ let ltiResponse;
+ if(res.headers.location!=null){
+ ltiResponse = {
+ ltiResponseURL: res.headers.location,
+ ltiResponseHTML: ' ',
+ ltiURL : params.ltiURL,
+ ltiKey : params.ltiKey,
+ ltiWidth: params.ltiWidth,
+ ltiHeight : params.ltiHeight
+ };
+ }
+ else {
+ ltiResponse = {
+ ltiResponseURL: '',
+ ltiResponseHTML: body,
+ ltiURL : params.ltiURL,
+ ltiKey : params.ltiKey,
+ ltiWidth: params.ltiWidth,
+ ltiHeight : params.ltiHeight
+ };
+ }
+ callback(null, ltiResponse);
+
+ }); //end on
+ });
+
+ console.log('Setting req error callback');
+ req.on('error', (err) => {
+ console.log('problem with request: ' + err.message);
+ callback(err);
+ });
+
+ // write data to request body
+ req.write(post_data);
+ console.log('Written to request.');
+ req.end();
+ console.log('Request ended.');
+ }//end if(resource=== 'lti')
+ }
+};
diff --git a/services/userProfile.js b/services/userProfile.js
index 568439aba..9f3f5fbd3 100644
--- a/services/userProfile.js
+++ b/services/userProfile.js
@@ -25,6 +25,7 @@ export default {
},
update: (req, resource, params, body, config, callback) => {
+ //console.log('userProfile.resource='+resource);
req.reqId = req.reqId ? req.reqId : -1;
log.info({Id: req.reqId, Service: __filename.split('/').pop(), Resource: resource, Operation: 'update', Method: req.method});
if (resource === 'userProfile.updatePassword') {
@@ -144,7 +145,61 @@ export default {
})
.then((body) => callback(null, body))
.catch((err) => callback(err));
- } else {
+ } else if (resource === 'userProfile.saveUserlti') {
+ console.log('userProfile.saveUserlti');
+ //prepare data
+ if (params.members === null || params.members === undefined)
+ params.members = [];
+ let members = params.members.reduce((prev, curr) => {
+ let member = {
+ userid: curr.userid,
+ joined: curr.joined || ''
+ };
+ prev.push(member);
+ return prev;
+ }, []);
+ let tosend = {
+ id: params.id,
+ key: params.key,
+ secret: !isEmpty(params.secret) ? params.secret : '',
+ isActive: !isEmpty(params.isActive) ? params.isActive : true,
+ timestamp: !isEmpty(params.timestamp) ? params.timestamp : '',
+ members: members,
+ referenceDateTime: (new Date()).toISOString()
+ };
+ // console.log('sending:', tosend, params.jwt);
+ rp({
+ method: 'PUT',
+ uri: Microservices.user.uri + '/userlti/createorupdate',
+ headers: { '----jwt----': params.jwt },
+ json: true,
+ body: tosend,
+ timeout: body.timeout
+ })
+ .then((body) => callback(null, body))
+ .catch((err) => callback(err));
+ } else if (resource === 'userProfile.deleteUserlti') {
+ rp({
+ method: 'DELETE',
+ uri: Microservices.user.uri + '/userlti/' + params.ltiid,
+ headers: { '----jwt----': params.jwt },
+ json: true,
+ timeout: body.timeout
+ })
+ .then((body) => callback(null, body))
+ .catch((err) => callback(err));
+ } else if (resource === 'userProfile.leaveUserlti') {
+ rp({
+ method: 'PUT',
+ uri: Microservices.user.uri + '/userlti/' + params.ltiid + '/leave',
+ headers: { '----jwt----': params.jwt },
+ json: true,
+ timeout: body.timeout
+ })
+ .then((body) => callback(null, body))
+ .catch((err) => callback(err));
+ }
+ else {
callback('failure');
}
},
@@ -308,7 +363,7 @@ export default {
}).catch((err) => callback(err));
} else {
if (params.loggedInUser === params.username || params.id === params.username) {
- // console.log('trying to get private user with id: ', params);
+ //console.log('trying to get private user with id: ', params);
rp({
method: 'GET',
uri: Microservices.user.uri + '/user/' + params.id + '/profile',
@@ -332,7 +387,8 @@ export default {
hasPassword: body.hasPassword || false,
providers: body.providers || [],
groups: !isEmpty(body.groups) ? body.groups : [],
- displayName: !isEmpty(body.displayName) ? body.displayName : ''
+ displayName: !isEmpty(body.displayName) ? body.displayName : '',
+ ltis: !isEmpty(body.ltis) ? body.ltis : []
};
callback(null, converted, {
headers: {
diff --git a/services/userlti.js b/services/userlti.js
new file mode 100644
index 000000000..f6ea96722
--- /dev/null
+++ b/services/userlti.js
@@ -0,0 +1,37 @@
+import { Microservices } from '../configs/microservices';
+import rp from 'request-promise';
+
+export default {
+ name: 'userlti',
+ // At least one of the CRUD methods is Required
+ read: (req, resource, params, config, callback) => {
+ // console.log('service usergroup with parameters',resource, params, config);
+ let args = params.params ? params.params : params;
+
+ // user groups owned by the specified user
+ if(resource === 'userlti.member'){
+ rp({
+ method: 'GET',
+ uri: Microservices.user.uri + '/user/' + args.userId + '/profile',
+ headers: { '----jwt----': args.jwt },
+ json: true
+ }).then( (response) => callback(null, response.ltis))
+ .catch( (err) => callback(err));
+ } else {
+ // usergroup.read got here
+ rp.post({
+ uri: Microservices.user.uri + '/userltis',
+ body: [params.ltiid],
+ json: true
+ })
+ .then((res) => {
+ // console.log('Got usergroups:', res);
+ callback(null, res);
+ })
+ .catch((err) => {
+ callback(err,null);
+ });
+ }
+
+ }
+};
diff --git a/stores/SlideEditStore.js b/stores/SlideEditStore.js
index 51655fd83..2de182f38 100644
--- a/stores/SlideEditStore.js
+++ b/stores/SlideEditStore.js
@@ -35,6 +35,13 @@ class SlideEditStore extends BaseStore {
this.embedHeight = '';
this.embedURL = '';
this.embedCode = '';
+ this.ltiClick = 'false';
+ this.ltiWidth = '';
+ this.ltiHeight = '';
+ this.ltiURL = '';
+ this.ltiKey = '';
+ this.ltiResponseURL = '',
+ this.ltiResponseHTML = '',
this.embedTitle = '';
this.HTMLEditorClick = 'false';
this.scaleRatio = null;
@@ -193,6 +200,18 @@ class SlideEditStore extends BaseStore {
this.HTMLEditorClick = 'false';
this.emitChange();
}
+ handleLTIAddClick(payload){
+ this.ltiURL = payload.ltiURL;
+ this.ltiKey = payload.ltiKey;
+ this.ltiWidth = payload.ltiWidth;
+ this.ltiHeight = payload.ltiHeight;
+ this.ltiResponseURL = payload.ltiResponseURL;
+ this.ltiResponseHTML = payload.ltiResponseHTML;
+ this.ltiClick = 'true';
+ this.emitChange();
+ this.ltiClick = 'false';
+ this.emitChange();
+ }
handleEmbedQuestions(payload){
//embedQuestionsContent - this is the content that will be embedded (questions and options)
//embedQuestions - this will be the trigger that causes the questions to be embedded.
@@ -243,6 +262,13 @@ class SlideEditStore extends BaseStore {
embedTitle: this.embedTitle,
embedWidth: this.embedWidth,
embedHeight: this.embedHeight,
+ ltiClick: this.ltiClick,
+ ltiURL: this.ltiURL,
+ ltiKey: this.ltiKey,
+ ltiWidth: this.ltiWidth,
+ ltiHeight: this.ltiHeight,
+ ltiResponseURL: this.ltiResponseURL,
+ ltiResponseHTML: this.ltiResponseHTML,
HTMLEditorClick: this.HTMLEditorClick,
scaleRatio: this.scaleRatio,
contentEditorFocus: this.contentEditorFocus,
@@ -286,6 +312,13 @@ class SlideEditStore extends BaseStore {
this.embedTitle = state.embedTitle;
this.embedWidth = state.embedWidth;
this.embedHeight = state.embedHeight;
+ this.ltiClick = state.ltiClick;
+ this.ltiURL = state.ltiURL;
+ this.ltiKey = state.ltiKey;
+ this.ltiWidth = state.ltiWidth;
+ this.ltiHeight = state.ltiHeight;
+ this.ltiResponseURL = state.ltiResponseURL;
+ this.ltiResponseHTML = state.ltiResponseHTML;
this.HTMLEditorClick = state.HTMLEditorClick;
this.scaleRatio = state.scaleRatio = 1;
this.contentEditorFocus = state.contentEditorFocus;
@@ -334,6 +367,7 @@ SlideEditStore.handlers = {
'CODE_CLICK': 'handleCodeClick',
'REMOVE_BACKGROUND_CLICK': 'handleRemoveBackgroundClick',
'EMBED_CLICK': 'handleEmbedClick',
+ 'ADD_LTI_SUCCESS': 'handleLTIAddClick',
'CHANGE_TITLE': 'changeTitle',
'HTML_EDITOR_CLICK': 'handleHTMLEditorClick',
'SLIDE_EMBED_QUESTIONS': 'handleEmbedQuestions',
diff --git a/stores/UserProfileStore.js b/stores/UserProfileStore.js
index f53ac5354..f3aa1d953 100644
--- a/stores/UserProfileStore.js
+++ b/stores/UserProfileStore.js
@@ -44,10 +44,21 @@ class UserProfileStore extends BaseStore {
this.currentUsergroup = {};
this.saveUsergroupError = '';
this.saveUsergroupIsLoading = false;
+
+ this.currentUserlti = {};
+ this.saveUserltiError = '';
+ this.saveUserltiIsLoading = false;
+
+ this.cancelUserltiError = '';
+
+
this.saveProfileIsLoading = false;
this.deleteUsergroupError = '';
this.usergroupsViewStatus = '';
+ this.deleteUserltiError = '';
+ this.userltisViewStatus = '';
+
let user = dispatcher.getContext().getUser();
//console.log('UserProfileStore constructor:', user);
try {
@@ -99,10 +110,19 @@ class UserProfileStore extends BaseStore {
this.currentUsergroup = {};
this.saveUsergroupError = '';
this.saveUsergroupIsLoading = false;
+
+ this.currentUserlti = {};
+ this.saveUserltiError = '';
+ this.saveUserltiIsLoading = false;
+ this.cancelUserltiError = '';
+
this.saveProfileIsLoading = false;
+
this.deleteUsergroupError = '';
this.usergroupsViewStatus = '';
+ this.deleteUserltiError = '';
+ this.userltisViewStatus = '';
//LoginModal
this.showLoginModal = false;
@@ -135,9 +155,19 @@ class UserProfileStore extends BaseStore {
currentUsergroup: this.currentUsergroup,
saveUsergroupError: this.saveUsergroupError,
saveUsergroupIsLoading: this.saveUsergroupIsLoading,
+
+ currentUserlti: this.currentUserlti,
+ saveUserltiError: this.saveUserltiError,
+ saveUserltiIsLoading: this.saveUserltiIsLoading,
+ cancelUserltiError: this.cancelUserltiError,
+
saveProfileIsLoading: this.saveProfileIsLoading,
deleteUsergroupError: this.deleteUsergroupError,
usergroupsViewStatus: this.usergroupsViewStatus,
+
+ deleteUserltiError: this.deleteUserltiError,
+ userltisViewStatus: this.userltisViewStatus,
+
showDeactivateAccountModal: this.showDeactivateAccountModal
};
}
@@ -174,6 +204,16 @@ class UserProfileStore extends BaseStore {
this.saveProfileIsLoading = state.saveProfileIsLoading;
this.deleteUsergroupError = state.deleteUsergroupError;
this.usergroupsViewStatus = state.usergroupsViewStatus;
+
+ this.currentUserlti = state.currentUserlti;
+ this.saveUserltiError = state.saveUserltiError;
+ this.saveUserltiIsLoading = state.saveUserltiIsLoading;
+ this.cancelUserltiError = state.cancelUserltiError;
+
+ this.deleteUserltiError = state.deleteUserltiError;
+ this.userltisViewStatus = state.userltisViewStatus;
+
+
this.showDeactivateAccountModal = state.showDeactivateAccountModal;
}
@@ -195,12 +235,16 @@ class UserProfileStore extends BaseStore {
}
fillInUser(payload) {
+ //console.log('UserProfileStore.fillInUser.payload='+JSON.stringify(payload));
+ //console.log('UserProfileStore.fillInUser called');
if(this.username === payload.uname)
this.userpicture = payload.picture;
if(!payload.onlyPicture){
Object.assign(this.user, payload);
this.category = payload.category;
}
+ this.user.email = payload.email;
+ //console.log('UserProfileStore.fillInUser.this.user.email='+this.user.email);
this.emitChange();
}
@@ -329,11 +373,89 @@ class UserProfileStore extends BaseStore {
this.emitChange();
}
+ updateUsergroup(group) {
+ this.currentUsergroup = group;
+ // console.log('UserProfileStore: updateUsergroup', group);
+ this.saveUsergroupError = '';
+ this.deleteUsergroupError = '';
+ this.emitChange();
+ }
+
+ updateUserlti(lti) {
+ this.currentUserlti = lti;
+ console.log('UserProfileStore: updateUserlti', lti);
+ this.saveUserltiError = '';
+ this.deleteUserltiError = '';
+ this.emitChange();
+ }
+
+
+ saveUsergroupFailed(error) {
+ this.saveUsergroupIsLoading = false;
+ this.saveUsergroupError = error.message;
+ this.emitChange();
+ }
+
+ saveUserltiFailed(error) {
+ this.saveUserltiIsLoading = false;
+ this.saveUserltiError = error.message;
+ this.emitChange();
+ }
+
+
+ saveUsergroupSuccess() {
+ this.saveUsergroupIsLoading = false;
+ this.currentUsergroup = {};
+ this.saveUsergroupError = '';
+ this.emitChange();
+ }
+
+ saveUserltiSuccess() {
+ this.saveUserltiIsLoading = false;
+ this.currentUserlti = {};
+ this.saveUserltiError = '';
+ this.emitChange();
+ }
+
+ saveUsergroupStart() {
+ this.saveUsergroupIsLoading = true;
+ this.emitChange();
+ }
+
+ saveUserltiStart() {
+ this.saveUserltiIsLoading = true;
+ this.emitChange();
+ }
+
saveProfileStart() {
this.saveProfileIsLoading = true;
this.emitChange();
}
+ cancelUserltiSuccess() {
+ this.currentUserlti = {};
+ this.cancelUserltiError = '';
+ this.emitChange();
+ }
+
+ deleteUsergroupFailed(error) {
+ this.deleteUsergroupError = {
+ action: 'delete',
+ message: error.message
+ };
+ this.usergroupsViewStatus = '';
+ this.emitChange();
+ }
+
+ deleteUserltiFailed(error) {
+ this.deleteUserltiError = {
+ action: 'delete',
+ message: error.message
+ };
+ this.userltisViewStatus = '';
+ this.emitChange();
+ }
+
deleteUsergroupSuccess(groupid) {
console.log('UserProfileStore deleteUsergroupSuccess: delete % from %', groupid, this.user.groups);
//remove group from user
@@ -348,11 +470,30 @@ class UserProfileStore extends BaseStore {
this.emitChange();
}
+ deleteUserltiSuccess(ltiid) {
+ console.log('UserProfileStore deleteUserltiSuccess: delete % from %', ltiid, this.user.ltis);
+ //remove lti from user
+ let ltis = this.user.ltis.reduce((prev, curr) => {
+ if (curr._id.toString() !== ltiid.toString())
+ prev.push(curr);
+ return prev;
+ }, []);
+ this.user.ltis = ltis;
+ this.deleteUserltiError = '';
+ this.userltisViewStatus = '';
+ this.emitChange();
+ }
+
updateUsergroupsStatus() {
this.usergroupsViewStatus = 'pending';
this.emitChange();
}
+ updateUserltisStatus() {
+ this.userltisViewStatus = 'pending';
+ this.emitChange();
+ }
+
setUserDecksLoading(){
this.userDecks = undefined;
// preserve sorting of sort dropdown during loading
@@ -433,7 +574,26 @@ UserProfileStore.handlers = {
'UPDATE_USERGROUPS_STATUS': 'updateUsergroupsStatus',
'LEAVE_USERGROUP_FAILED': 'deleteUsergroupFailed',
'LEAVE_USERGROUP_SUCCESS': 'deleteUsergroupSuccess',
- 'SAVE_USERPROFILE_START': 'saveProfileStart'
+ 'SAVE_USERPROFILE_START': 'saveProfileStart',
+
+ //LTI
+ 'UPDATE_USERLTI': 'updateUserlti',
+ 'SAVE_USERLTI_START': 'saveUserltiStart',
+ 'SAVE_USERLTI_FAILED': 'saveUserltiFailed',
+ 'SAVE_USERLTI_SUCCESS': 'saveUserltiSuccess',
+
+
+ 'CANCEL_USERLTI_SUCCESS': 'cancelUserltiSuccess',
+
+ 'DELETE_USERLTI_FAILED': 'deleteUserltiFailed',
+ 'DELETE_USERLTI_SUCCESS': 'deleteUserltiSuccess',
+ 'UPDATE_USERLTIS_STATUS': 'updateUserltisStatus',
+ 'LEAVE_USERLTI_FAILED': 'deleteUserltiFailed',
+ 'LEAVE_USERLTI_SUCCESS': 'deleteUserltiSuccess',
+
+ 'SAVE_USERPROFILE_START': 'saveProfileStart',
+ 'SHOW_DEACTIVATE_ACCOUNT_MODAL': 'showDeactivateModal',
+ 'HIDE_DEACTIVATE_ACCOUNT_MODAL': 'hideDeactivateModal',
};
export default UserProfileStore;