Skip to content

Commit

Permalink
Merge pull request #53 from schulcloud/168-improve-lti-concept
Browse files Browse the repository at this point in the history
168 improve lti concept
  • Loading branch information
Langleu authored Dec 15, 2016
2 parents 5c56911 + 10d3a11 commit 974e9fd
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 67 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.1.18",
"babel-preset-react": "^6.1.18",
"crypto": "0.0.3",
"express": "^4.14.0",
"express-urlrewrite": "^1.2.0",
"feathers-authentication": "^0.7.11",
Expand All @@ -29,7 +30,8 @@
"feathers-hooks": "^1.6.1",
"feathers-subscriptions-manager": "^1.0.1",
"feathers-reactive": "^0.4.1",
"jquery": "^3.1.1",
"oauth-1.0a": "^2.0.0",
"jquery": "^3.1.1",
"react": "^0.14.8",
"react-dom": "^0.14.8",
"react-komposer": "^1.13.1",
Expand Down
4 changes: 3 additions & 1 deletion src/modules/core/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import Config from './config';
import Permissions from './permissions';
import Server from './server';
import Notification from './notification';
import LTICustomer from './ltiCustomer';

export {
App,
Config,
Permissions,
Server,
Notification
Notification,
LTICustomer
};
65 changes: 65 additions & 0 deletions src/modules/core/helpers/ltiCustomer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const OAuth = require('oauth-1.0a');
const crypto = require('crypto');

const ltiRoles = {
user: 'Learner',
student: 'Learner',
teacher: 'Instructor',
administrator: 'Administrator',
superhero: 'Administrator'
};

class LTICustomer {
constructor() {}

createConsumer(key, secret) {
return OAuth({
consumer: {
key: key,
secret: secret
},
signature_method: 'HMAC-SHA1',
hash_function: function(base_string, key) {
return crypto.createHmac('sha1', key).update(base_string).digest('base64');
}
});
}

mapSchulcloudRoleToLTIRole(role) {
return ltiRoles[role];
}

sendRequest(request_data, consumer) {
var name,
form = document.createElement("form"),
node = document.createElement("input");


form.action = request_data.url;
form.method = request_data.method;
form.target = "_blank";

var formData = consumer.authorize(request_data);

for (name in formData) {
node.name = name;
node.value = formData[name].toString();
form.appendChild(node.cloneNode());
}

// To be sent, the form needs to be attached to the main document.
form.style.display = "none";
document.body.appendChild(form);

form.submit();

// But once the form is sent, it's useless to keep it.
document.body.removeChild(form);
}

customFieldToString(custom) {
return `custom_${custom.key}`;
}
}

export default new LTICustomer();
21 changes: 21 additions & 0 deletions src/modules/tools/actions/newTool.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import {browserHistory} from 'react-router';
import { Server } from '../../core/helpers';

const toolsConnectService = Server.service('/ltiTools/connect');
const toolService = Server.service('/ltiTools');

export default {
createNew: (tool) => {
toolService.create(tool)
.then(result => {
// Todo: remove when subsmanager is implemented
window.location.href = '/tools/'
})
.catch(err => {
console.log(err);
});
}
};


58 changes: 34 additions & 24 deletions src/modules/tools/actions/tools.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import React from 'react';
import {browserHistory} from 'react-router';
import { Server } from '../../core/helpers';

const toolsConnectService = Server.service('/ltiTools/connect');
const toolService = Server.service('/ltiTools');
import { Server, LTICustomer } from '../../core/helpers';
const rolesService = Server.service('/roles');

export default {
connect: (toolId) => {
let win = window.open("");
toolsConnectService.create({ toolId })
.then(result => {
if (result.type === 'url') {
win.location.href = result.data;
} else {
win.document.write(result.data);
}
});
},
createNew: (tool) => {
toolService.create(tool)
.then(result => {
// Todo: remove when subsmanager is implemented
window.location.href = '/tools/';
})
.catch(err => {
console.log(err);

connect: (tool) => {
var consumer = LTICustomer.createConsumer(tool.key, tool.secret);

const currentUser = Server.get('user');

// todo: do this better
rolesService.find({query: {'_id': currentUser.roles[0]}}).then((result) => {
let role = result.data[0].name;
var payload = {
lti_version: tool.lti_version,
lti_message_type: tool.lti_message_type,
resource_link_id: tool.resource_link_id,
user_id: currentUser._id,
roles: LTICustomer.mapSchulcloudRoleToLTIRole(role),
launch_presentation_document_target: 'window',
lis_person_name_full: 'John Logie Baird', // todo: get from user, wait for populate
lis_person_contact_email_primary: 'jbaird@uni.ac.uk',
launch_presentation_locale: 'en'
};

tool.customs.forEach((custom) => {
payload[LTICustomer.customFieldToString(custom)] = custom.value;
});

var request_data = {
url: tool.url,
method: 'POST',
data: payload
};

LTICustomer.sendRequest(request_data, consumer);
});
}
};

Expand Down
33 changes: 33 additions & 0 deletions src/modules/tools/components/newTool.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import LayoutBackend from '../../backend/containers/layout';
import SectionTitle from '../../backend/components/title'; /* only for backend */
import {browserHistory} from 'react-router';
import TemplateToolCard from './templateToolCard';

require('../styles/tools.scss');

class NewTool extends React.Component {

constructor(props) {
super(props);
}

render() {
let idCount = 0;
return (
<LayoutBackend className="tools">
<SectionTitle title="Tool Vorlagen"/>
<div className="tools-section">
{
this.props.tools.map((tool) => {
idCount++;
return <TemplateToolCard {...this.props} key={idCount} modalId={idCount} tool={tool} />;
})
}
</div>
</LayoutBackend>
);
}

}

export default NewTool;
32 changes: 11 additions & 21 deletions src/modules/tools/components/newToolForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ class NewToolForm extends React.Component {
constructor(props) {
super(props);
this.state = {
tool: {
lti_message_type: 'basic-lti-launch-request',
lti_version: 'LTI-1p0',
resource_link_id: '0',
privacy_permission: 'anonymous',
customs: []
},
custom_fields: [],
tool: this.props.toolTemplate,
custom_fields: this.props.toolTemplate.customs.map((c) => {
c.id = this.createRandomComponentId(c.key, c.value);
return c;
}),
new_custom_field: {
key: '',
value: ''
Expand All @@ -26,6 +23,10 @@ class NewToolForm extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
}

createRandomComponentId(key, value) {
return key + value + Math.random() * 10000;
}

handleChange(fieldName, event) {
var stateUpdate = this.state.tool;
stateUpdate[fieldName] = event.target.value;
Expand All @@ -50,7 +51,8 @@ class NewToolForm extends React.Component {
var stateCustomsUpdate = this.state.custom_fields;
var newCustomField = this.state.new_custom_field;

var index = newCustomField.key + newCustomField.value + Math.random() * 10000;
// index for react component, not id of real db object
var index = this.createRandomComponentId(newCustomField.key, newCustomField.value);
stateCustomsUpdate.push({id: index, key: newCustomField.key, value: newCustomField.value});

this.setState({custom_fields: stateCustomsUpdate});
Expand Down Expand Up @@ -97,18 +99,6 @@ class NewToolForm extends React.Component {
Name (Pflichtfeld):
<input type="text" required="required" value={this.state.tool.name} onChange={this.handleChange.bind(null, "name")} />
</label> <br></br>
<label>
URL (Pflichtfeld):
<input type="text" required="required" value={this.state.tool.url} onChange={this.handleChange.bind(null, "url")} />
</label> <br></br>
<label>
OAuth-Key (Pflichtfeld):
<input type="text" required="required" value={this.state.tool.key} onChange={this.handleChange.bind(null, "key")} />
</label> <br></br>
<label>
OAuth-Secret (Pflichtfeld):
<input type="text" required="required" value={this.state.tool.secret} onChange={this.handleChange.bind(null, "secret")} />
</label> <br></br>
<label>
Logo (URL):
<input type="text" value={this.state.tool.logo_url} onChange={this.handleChange.bind(null, "logo_url")} />
Expand Down
48 changes: 48 additions & 0 deletions src/modules/tools/components/templateToolCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Link} from 'react-router';
import NewToolForm from './newToolForm';
require('../styles/toolCard.scss');
require('../../../static/images/cloud.png');

class ToolCard extends React.Component {

constructor(props) {
super(props);
this.state = {};
}

render() {
var tool = this.props.tool;
return (
<div>
<Link className="col-sm-4 tool-card" data-toggle="modal" data-target={"#newToolModal" + this.props.modalId}>
<div className="card">
{ tool.logo_url
? <img className="card-img-top" src={tool.logo_url} alt="Card image cap"/>
: <img className="card-img-top" src="/images/cloud.png" alt="Card image cap"/>
}
<div className="card-block">
<h4 className="card-title">{tool.name}</h4>
</div>
</div>
</Link>

<div className="modal fade" id={"newToolModal" + this.props.modalId} role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 className="modal-title" id="myModalLabel">Neues LTI-Tool erstellen</h4>
</div>
<div className="modal-body">
<NewToolForm toolTemplate={tool} modal="#"{...this.props} />
</div>
</div>
</div>
</div>
</div>
);
}
}

export default ToolCard;
2 changes: 1 addition & 1 deletion src/modules/tools/components/toolCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ToolCard extends React.Component {
}

handleConnect(e) {
this.props.actions.connect(this.props.tool._id);
this.props.actions.connect(this.props.tool);
}


Expand Down
25 changes: 8 additions & 17 deletions src/modules/tools/components/tools.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import LayoutBackend from '../../backend/containers/layout';
import SectionTitle from '../../backend/components/title'; /* only for backend */
import ToolCard from './toolCard';
import {browserHistory} from 'react-router';
import NewToolForm from './newToolForm';

import { Permissions, Server } from '../../core/helpers/';
import permissions from '../permissions';
require('../styles/tools.scss');

class Tools extends React.Component {
Expand All @@ -16,6 +16,11 @@ class Tools extends React.Component {
browserHistory.push("/tools/new/");
}

handleHasPermission(e) {
const currentUser = Server.get('user');
return Permissions.userHasPermission(currentUser, permissions.NEW_VIEW);
}

render() {
return (
<LayoutBackend className="tools">
Expand All @@ -27,21 +32,7 @@ class Tools extends React.Component {
})
}
</div>
<button type="button" data-toggle="modal" data-target="#newToolModal" className="btn btn-primary">Neues Tool erstellen</button>

<div className="modal fade" id="newToolModal" role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 className="modal-title" id="myModalLabel">Neues LTI-Tool erstellen</h4>
</div>
<div className="modal-body">
<NewToolForm modal="#"{...this.props} />
</div>
</div>
</div>
</div>
<button type="button" style={{visibility: this.handleHasPermission() ? 'visible' : 'hidden'}} onClick={this.handleCreateNew.bind(this)} className="btn btn-primary btn-tools">Neues Tool erstellen</button>
</LayoutBackend>
);
}
Expand Down
Loading

0 comments on commit 974e9fd

Please sign in to comment.