Skip to content

Commit 798118d

Browse files
authored
Merge pull request #11 from udzuki/feat-10-lrs-api
feat: 機関ごとのLRSをLearning LockerのREST APIから取得
2 parents 2b33b5e + fdc4b6b commit 798118d

6 files changed

Lines changed: 85 additions & 100 deletions

File tree

README.adoc

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
:lang: ja
33
:source-highlighter: rouge
44
:author: 国立情報学研究所
5-
:revdate: 2022年1月20日
6-
:revnumber: 3.0.0版
5+
:revdate: 2022年1月31日
6+
:revnumber: 3.1-SNAPSHOT版
77
:doctype: book
88
:version-label:
99
:chapter-label:
@@ -150,14 +150,12 @@ image::learninglocker/new-xapi-store-client.png[align=center]
150150
----
151151
const config = {
152152
153-
LRS:{
153+
LRS: {
154154
...
155-
clients:{
156-
// LRS client
157-
'default':{
158-
user:'<xapi-store-client-key>', // <1>
159-
pass:'<xapi-store-client-secret>' // <1>
160-
},
155+
client: {
156+
key: '<xapi-store-client-key>', // <1>
157+
secret: '<xapi-store-client-secret>' // <1>
158+
},
161159
162160
----
163161
<1> <<learninglocker_client_settings>>で確認したLRSのクライアント情報を設定

moodle_video_logs/README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
:source-highlighter: rouge
44
:author: 国立情報学研究所
55
:revdate: 2021年7月29日
6-
:revnumber: 3.0.0版
6+
:revnumber: 3.1-SNAPSHOT版
77
:doctype: book
88
:version-label:
99
:chapter-label:

moodle_video_logs/STATEMENT_SPEC.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
:source-highlighter: rouge
44
:author: 国立情報学研究所
55
:revdate: 2021年7月29日
6-
:revnumber: 3.0.0版
6+
:revnumber: 3.1-SNAPSHOT版
77
:doctype: book
88
:version-label:
99
:chapter-label:

xapi_stmt_gen/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM node:12.22.0
22

3-
LABEL version="2.6.0"
3+
LABEL version="2.7.0"
44

55
WORKDIR /usr/local/src
66
RUN mkdir xapi_stmt_gen
Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,48 @@
11
const config = {
2+
url: 'http://learninglocker',
23
limit: 500,
34
chunkSize: 100,
4-
db:{
5+
db: {
56
// LMS database connection settings
6-
lms:{
7-
host:'moodle-docker_db_1',
8-
port:5432,
9-
database:'moodle',
10-
username:'moodleuser',
11-
password:'m@0dl3ing',
12-
prefix:'mdl_'
7+
lms: {
8+
host: 'moodle-docker_db_1',
9+
port: 5432,
10+
database: 'moodle',
11+
username: 'moodleuser',
12+
password: 'm@0dl3ing',
13+
prefix: 'mdl_'
1314
},
14-
la:{
15-
host:'learning_analytics_db',
16-
port:5432,
17-
database:'learning_analytics',
18-
username:'learning_analytics',
19-
password:'learning_analytics',
15+
la: {
16+
host: 'learning_analytics_db',
17+
port: 5432,
18+
database: 'learning_analytics',
19+
username: 'learning_analytics',
20+
password: 'learning_analytics',
2021
}
2122
},
22-
LRS:{
23-
url:'http://learninglocker:8081/data/xAPI/',
24-
clients:{
25-
// LRS client
26-
'default':{
27-
user:'031eb8dccd9729d6a8a16d245b4d1dddf1e2ded7',
28-
pass:'a94fd8e44662fe7cd50cd53812dad84a4a81ab9b'
29-
},
30-
/**
31-
* This 'scoped' setting can be used to send
32-
* statements to separated LRSes based on ePPN in GakuNin.
33-
*/
34-
//'scoped':[
35-
// {
36-
// scope:'foo.co.jp',
37-
// user:'dc052567a686bb93a7e2fb9547ed6f6974171e8b',
38-
// pass:'b0a3f0e04294c258cb3ce8bdbb55d24c6bad15f8'
39-
// },
40-
// {
41-
// scope:'bar.ac.jp',
42-
// user:'74abe7278e28aef9b5548d90700ee423e36c1fbb',
43-
// pass:'8a34c7f8433d51ede87f533afe672d6e357c8d37'
44-
// }
45-
//]
46-
}
23+
LRS: {
24+
// LRS client with 'Overall Scopes: API All' checked
25+
client: {
26+
key: '',
27+
secret: ''
28+
},
29+
// Store statements in each LRS with title matching actor's ePPN scope
30+
ePPNScoped: false,
4731
},
48-
category:{
49-
id:'http://moodle.org',
50-
definition:{
51-
type:'http://id.tincanapi.com/activitytype/source',
52-
name:'Moodle',
32+
category: {
33+
id: 'http://moodle.org',
34+
definition: {
35+
type: 'http://id.tincanapi.com/activitytype/source',
36+
name: 'Moodle',
5337
description:
5438
'Moodle is a open source learning platform designed to ' +
5539
'provide educators, administrators and learners with ' +
5640
'a single robust, secure and integrated system to create ' +
5741
'personalized learning environments.'
5842
}
5943
},
60-
platform:'Moodle',
61-
language:'en',
44+
platform: 'Moodle',
45+
language: 'en',
6246
homepage: 'http://localhost:8000'
6347
};
6448
module.exports = config;

xapi_stmt_gen/xapi_stmt_gen/generator.js

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,7 @@ axios.defaults.httpAgent = new http.Agent({
10071007
async function sendStatements(statements, lrs) {
10081008
return axios({
10091009
'method': 'POST',
1010-
'url': lrs.url + 'statements',
1010+
'url': `${config.url}:8081/data/xAPI/statements`,
10111011
'auth': lrs.auth,
10121012
'headers': {
10131013
'Content-Type': 'application/json',
@@ -1017,42 +1017,48 @@ async function sendStatements(statements, lrs) {
10171017
});
10181018
}
10191019

1020-
/**
1021-
* Checks if LRSes are scoped.
1022-
*/
1023-
function isScoped() {
1024-
if ('scoped' in config.LRS.clients) {
1025-
return true;
1026-
}
1027-
return false;
1028-
}
1020+
let LRS_CLIENTS;
10291021

10301022
/**
1031-
* Returns LRS settings found by scope.
1023+
* Returns LRS clients.
10321024
*/
1033-
function getLRS(scope) {
1034-
let lrses = {};
1035-
if ('default' in config.LRS.clients) {
1036-
lrses['default'] = {
1037-
'url':config.LRS.url,
1038-
'auth':{
1039-
'username':config.LRS.clients.default.user,
1040-
'password':config.LRS.clients.default.pass
1041-
},
1042-
};
1043-
}
1044-
if ('scoped' in config.LRS.clients) {
1045-
config.LRS.clients.scoped.forEach(client => {
1046-
lrses[client.scope] = {
1047-
'url':config.LRS.url,
1048-
'auth':{
1049-
'username':client.user,
1050-
'password':client.pass
1051-
},
1052-
};
1053-
});
1025+
async function getLRSClients() {
1026+
if (!(config.LRS.client.key && config.LRS.client.secret)) {
1027+
process.exitCode = 1;
1028+
throw new Error('Specify LRS client in config/app.js');
10541029
}
1055-
return lrses[scope];
1030+
const clientResponse = await axios.get(`${config.url}:3000/api/v2/client`, {
1031+
'auth': {
1032+
'username':config.LRS.client.key,
1033+
'password':config.LRS.client.secret
1034+
}
1035+
}).catch(err => {
1036+
process.exitCode = 1;
1037+
throw new Error(`Failed to get LRS clients - ${err}`);
1038+
});
1039+
const lrsResponse = await axios.get(`${config.url}:3000/api/v2/lrs`, {
1040+
'auth': {
1041+
'username':config.LRS.client.key,
1042+
'password':config.LRS.client.secret
1043+
}
1044+
}).catch(err => {
1045+
process.exitCode = 1;
1046+
throw new Error(`Failed to get LRSes - ${err}`);
1047+
});
1048+
let clients = {}
1049+
lrsResponse.data.forEach((lrs) => {
1050+
const client = clientResponse.data.find(c => c.lrs_id === lrs._id)
1051+
const key = client.api.basic_key
1052+
const secret = client.api.basic_secret
1053+
const scope = (key === config.LRS.client.key && secret === config.LRS.client.secret) ? 'default' : lrs.title
1054+
clients[scope] = {
1055+
'auth': {
1056+
'username': key,
1057+
'password': secret
1058+
}
1059+
};
1060+
});
1061+
return clients;
10561062
}
10571063

10581064
/**
@@ -1069,11 +1075,10 @@ async function routeStatements(objecttable, xapis){
10691075
const statements = xapis[scope].map((value, index, array) => {
10701076
return value.statement;
10711077
});
1072-
const lrs = getLRS(scope);
1078+
const lrs = LRS_CLIENTS[scope];
10731079
const seq = objectids[0];
10741080
logger.info(
1075-
`[SEQ:${seq}][SCOPE:${scope}] Sending ${statements.length} statements ` +
1076-
`to ${config.LRS.url}statements...`
1081+
`[SEQ:${seq}][SCOPE:${scope}] Sending ${statements.length} statements...`
10771082
);
10781083
const promise = sendStatements(statements, lrs).then(() => {
10791084
logger.info(`[SEQ:${seq}][SCOPE:${scope}] ${statements.length} statements added.`);
@@ -1112,15 +1117,15 @@ function getScopeFromEppn(username) {
11121117
*/
11131118
function getLRSScope(userAttrs, userid) {
11141119
let scope = 'default';
1115-
if (isScoped()) {
1120+
if (config.LRS.ePPNScoped) {
11161121
scope = userid in userAttrs ? userAttrs[userid].scope : null;
11171122
if (!scope) {
11181123
logger.trace(
11191124
`The user(id:${userid}) doesn't have ` +
11201125
'an ePPN scope, sending the statement to the default LRS.'
11211126
);
11221127
scope = 'default';
1123-
} else if (!getLRS(scope)) {
1128+
} else if (!LRS_CLIENTS[scope]) {
11241129
logger.trace(
11251130
`LRS for ${scope} not found, ` +
11261131
'sending the statement to the default LRS.'
@@ -3420,6 +3425,7 @@ async function translateStandardLogs(logs, userAttrs, courseNames){ // eslint-di
34203425
);
34213426
continue;
34223427
}
3428+
34233429
const scope = getLRSScope(userAttrs, log.userid);
34243430
if (!(scope in xapis)) {
34253431
xapis[scope] = [];
@@ -3686,10 +3692,7 @@ const waitForInterval = () => {
36863692
* Processes logs until no more data exists.
36873693
*/
36883694
module.exports = async function main() { // eslint-disable-line max-statements
3689-
if (!('default' in config.LRS.clients) && !('scoped' in config.LRS.clients)) {
3690-
process.exitCode = 1;
3691-
throw new Error('Specify LRS clients in config/app.js');
3692-
}
3695+
LRS_CLIENTS = await getLRSClients();
36933696

36943697
// Retrieve all users
36953698
const users = await USER.findAll({

0 commit comments

Comments
 (0)