UNPKG

108 kBJavaScriptView Raw
1// CodeGradXlib
2// Time-stamp: "2019-03-26 18:25:20 queinnec"
3
4/** Javascript Library to interact with the CodeGradX infrastructure.
5
6## Installation
7
8```bash
9npm install codegradxlib
10```
11
12## Usage
13
14This library makes a huge usage of promises as may be seen in the following
15use case:
16
17```javascript
18// Example of use:
19const CodeGradX = require('codegradxlib');
20
21new CodeGradX.State(postInitializer);
22
23
24CodeGradX.getCurrentState().
25 // ask for user's login and password:
26 getAuthenticatedUser(login, password).
27 then(function (user) {
28 // let the user choose one campaign among user.getCampaigns()
29 // let us say that we choose campaign 'free':
30 user.getCampaign('free').
31 then(function (campaign) {
32 // let the user choose one exercise among campaign.getExercisesSet()
33 campaign.getExercise('some.exercise.name').
34 then(function (exercise) {
35 exercise.getDescription().
36 then(function (description) {
37 // display stem of exercise and get user's answer:
38 exercise.sendFileAnswer("some.filename").
39 then(function (job) {
40 // wait for the marking report:
41 job.getReport().
42 then(function (job) {
43 // display job.report
44```
45
46More details on the protocols and formats used to interact with the
47CodeGradX infrastructure can be found in the documentation of
48{@link http://paracamplus.com/CodeGradX/Resources/overview.pdf|CodeGradX}.
49
50If you want to use that module from Nodejs and have acces to the file
51system, require the accompanying module CodeGradXlib4node.
52
53
54@module codegradxlib
55@author Christian Queinnec <Christian.Queinnec@codegradx.org>
56@license MIT
57@see {@link http://codegradx.org/|CodeGradX} site.
58*/
59
60// Possible improvements:
61// - name differently methods returning a Promise from others
62
63
64const CodeGradX = {};
65
66/** Export the `CodeGradX` object */
67module.exports = CodeGradX;
68//export default CodeGradX;
69
70//const _ = require('lodash');
71const _ = (function () {
72 const isFunction = require('lodash/isFunction');
73 const forEach = require('lodash/forEach');
74 const isNumber = require('lodash/isNumber');
75 const memoize = require('lodash/memoize');
76 const forIn = require('lodash/forIn');
77 const has = require('lodash/has');
78 const filter = require('lodash/filter');
79 const map = require('lodash/map');
80 const reduce = require('lodash/reduce');
81 return { isFunction, forEach, isNumber, memoize, forIn,
82 has, filter, map, reduce };
83})();
84const when = require('when');
85const rest = require('rest');
86const mime = require('rest/interceptor/mime');
87const registry = require('rest/mime/registry');
88const xml2js = require('xml2js');
89const sax = require('sax');
90const he = require('he');
91const util = require('util');
92
93// Define that additional MIME type:
94registry.register('application/octet-stream', {
95 read: function(str) {
96 return str;
97 },
98 write: function(str) {
99 return str;
100 }
101 });
102
103/* Are we running under Node.js */
104CodeGradX.isNode = _.memoize(
105 // See http://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser
106 function _checkIsNode () {
107 /*jshint -W054 */
108 const code = "try {return this===global;}catch(e){return false;}";
109 const f = new Function(code);
110 return f();
111 });
112
113CodeGradX.checkIfHTTPS = function () {
114 /*jshint -W054 */
115 const code = "try {if (this===window) {return window.document.documentURI}}catch(e){return false;}";
116 const f = new Function(code);
117 const uri = f();
118 if ( uri ) {
119 // We are within a browser
120 return uri.match(/^https:/);
121 }
122 return false;
123};
124
125CodeGradX._str2num = function (str) {
126 if (!isNaN(str)) {
127 str = str % 1 === 0 ? parseInt(str, 10) : parseFloat(str);
128 }
129 return str;
130};
131
132CodeGradX._str2num2decimals = function (str) {
133 const scale = 100; // Leave two decimals
134 if ( _.isNumber(str) ) {
135 return (Math.round(scale * str))/scale;
136 } else if ( ! isNaN(str) ) {
137 if ( str % 1 === 0 ) {
138 return parseInt(str, 10);
139 } else {
140 let x = parseFloat(str);
141 return (Math.round(scale * x))/scale;
142 }
143 }
144};
145
146CodeGradX._str2Date = function (str) {
147 let ms = Date.parse(str);
148 if ( ! isNaN(ms) ) {
149 const d = new Date(ms);
150 //console.log("STR1:" + str + " => " + ms + " ==> " + d);
151 return d;
152 }
153 // Safari cannot Date.parse('2001-01-01 00:00:00+00')
154 // but can Date.parse('2001-01-01T00:00:00')
155 const rmtz = /^\s*(.+)([+]\d+)?\s*$/;
156 str = str.replace(rmtz, "$1").replace(/ /, 'T');
157 ms = Date.parse(str);
158 if ( ! isNaN(ms) ) {
159 const d = new Date(ms);
160 //console.log("STR2:" + str + " => " + ms + " ==> " + d);
161 return d;
162 }
163 throw new Error("Cannot parse Date " + str);
164};
165
166// On some browsers the ISO string shows the long name of the time zone:
167CodeGradX.Date2str = function (date) {
168 if ( date ) {
169 if ( date instanceof Date ) {
170 date = date.toISOString();
171 }
172 date = date.replace(/[.].*Z?$/, '')
173 .replace('T', ' ');
174 return date + 'Z';
175 }
176 return date;
177};
178
179/**
180 Compute duration in seconds between two dates (whether Date or String).
181 */
182
183CodeGradX.computeDuration = function (end, start) {
184 try {
185 if ( end instanceof Date ) {
186 end = end.getTime();
187 } else if ( typeof(end) === 'string' ||
188 end instanceof String ) {
189 end = Date.parse(end);
190 } else {
191 throw new Error("Unknown type of Date");
192 }
193 if ( start instanceof Date ) {
194 start = start.getTime();
195 } else if ( typeof(start) === 'string' ||
196 start instanceof String ) {
197 start = Date.parse(start);
198 } else {
199 throw new Error("Unknown type of Date");
200 }
201 return (end - start)/1000;
202 } catch (e) {
203 return undefined;
204 }
205};
206
207// **************** Log ********************************
208
209/** Record facts in a log. This is useful for debug!
210 A log only keeps the last `size` facts.
211 Use the `show` method to display it.
212 See also helper method `debug` on State to log facts.
213
214 @constructor
215 @property {Array<string>} items - array of kept facts
216 @property {number} size - maximal number of facts to keep in the log
217
218 */
219
220CodeGradX.Log = function () {
221 this.items = [];
222 this.size = 90;
223};
224
225/** Log some facts. The facts (the arguments) will be concatenated
226 (with a separating space) to form a string to be recorded in the log.
227
228 @method CodeGradX.Log.debug
229 @param {Value} arguments - facts to record
230 @returns {Log}
231 @lends CodeGradX.Log.prototype
232 @alias module:codegradxlib.debug
233 */
234
235CodeGradX.Log.prototype.debug = function () {
236 // Separate seconds from milliseconds:
237 let msg = (''+Date.now()).replace(/(...)$/, ".$1") + ' ';
238 for (let i=0 ; i<arguments.length ; i++) {
239 if ( arguments[i] === null ) {
240 msg += 'null ';
241 } else if ( arguments[i] === undefined ) {
242 msg += 'undefined ';
243 } else {
244 msg += util.inspect(arguments[i], { depth: 2 }) + ' ';
245 }
246 }
247 if ( this.items.length > this.size ) {
248 this.items.splice(0, 1);
249 }
250 this.items.push(msg);
251 return this;
252};
253
254/** Display the log with `console.log`
255 Console.log is asynchronous while writing in a file is synchronous!
256
257 @method show
258 @param {Array[object]} items - supersede the log with items
259 @param {string} filename - write in file rather than console.
260 @returns {Log}
261 @memberof {CodeGradX.Log#}
262
263 */
264
265CodeGradX.Log.prototype.show = function (items) {
266 // console.log is run later so take a copy of the log now to
267 // avoid displaying a later version of the log.
268 items = items || this.items.slice(0);
269 for ( let item of items ) {
270 console.log(item);
271 }
272 return this;
273};
274
275/** Display the log with `console.log` and empty it.
276
277 @method showAndRemove
278 @returns {Log}
279 @memberof {CodeGradX.Log#}
280
281 */
282
283CodeGradX.Log.prototype.showAndRemove = function (filename) {
284 // console.log is run later so take a copy of the log now to
285 // avoid displaying a later version of the log:
286 const items = this.items;
287 this.items = [];
288 return this.show(items, filename);
289};
290
291// **************** Global state *********************************
292
293/** The global state records the instantaneous state of the various
294 servers of the CodeGradX constellation. It also holds the current user,
295 cookie and campaign. The global `State` class is a singleton that may
296 be further customized with the `initializer` function. This singleton
297 can be obtained with `getCurrentState()`.
298
299 @constructor
300 @param {Function} initializer - optional customizer
301 @returns {State}
302
303 The `initializer` will be invoked with the state as first argument.
304 The result of initializer() will become the final state.
305
306 */
307
308CodeGradX.State = function (initializer) {
309 this.userAgent = rest.wrap(mime);
310 this.log = new CodeGradX.Log();
311 // State of servers:
312 this.servers = {
313 // The domain to be suffixed to short hostnames:
314 domain: '.codegradx.org',
315 // the shortnames of the four kinds of servers:
316 names: ['a', 'e', 'x', 's'],
317 // default protocol:
318 protocol: 'https',
319 // Descriptions of the A servers:
320 a: {
321 // Use that URI to check whether the server is available or not:
322 suffix: '/alive',
323 // Description of an A server:
324 0: {
325 // a full hostname supersedes the default FQDN:
326 host: 'a5.codegradx.org',
327 enabled: false
328 },
329 1: {
330 host: 'a4.codegradx.org',
331 enabled: false
332 },
333 2: {
334 host: 'a6.codegradx.org',
335 enabled: false
336 }
337 },
338 e: {
339 suffix: '/alive',
340 0: {
341 host: 'e5.codegradx.org',
342 enabled: false
343 },
344 1: {
345 host: 'e4.codegradx.org',
346 enabled: false
347 },
348 2: {
349 host: 'e6.codegradx.org',
350 enabled: false
351 }
352 },
353 x: {
354 suffix: '/dbalive',
355 0: {
356 host: 'x4.codegradx.org',
357 enabled: false
358 },
359 1: {
360 host: 'x5.codegradx.org',
361 enabled: false
362 },
363 2: {
364 host: 'x6.codegradx.org',
365 enabled: false
366 }
367 },
368 s: {
369 suffix: '/index.txt',
370 0: {
371 host: 's4.codegradx.org',
372 enabled: false
373 },
374 1: {
375 host: 's5.codegradx.org',
376 enabled: false
377 },
378 2: {
379 host: 's6.codegradx.org',
380 enabled: false
381 },
382 3: {
383 host: 's3.codegradx.org',
384 enabled: false,
385 once: true
386 }
387 }
388 };
389 // Current values
390 this.currentUser = null;
391 this.currentCookie = null;
392 // Post-initialization
393 let state = this;
394 // Cache for jobs useful when processing batches:
395 state.cache = {
396 jobs: {}
397 };
398 if ( _.isFunction(initializer) ) {
399 state = initializer.call(state, state);
400 }
401 let protocol = 'http';
402 if ( CodeGradX.checkIfHTTPS() ) {
403 // Make 'Upgrade Insecure Request' happy:
404 // and avoid "Blocked: mixed-content'
405 protocol = 'https';
406 }
407 state.servers.protocol = state.servers.protocol || protocol;
408 state.servers.a.protocol = state.servers.a.protocol ||
409 state.servers.protocol;
410 state.servers.e.protocol = state.servers.e.protocol ||
411 state.servers.protocol;
412 state.servers.s.protocol = state.servers.s.protocol ||
413 state.servers.protocol;
414 state.servers.x.protocol = state.servers.x.protocol ||
415 state.servers.protocol;
416 // Make the state global
417 CodeGradX.getCurrentState = function () {
418 return state;
419 };
420 return state;
421};
422
423/** Get the current state or create it if missing.
424 The initializer has type State -> State
425 This function will be replaced when the state is created.
426
427 @param {function} initializer - post-initialization of the state object
428 @returns {State}
429
430*/
431
432CodeGradX.getCurrentState = function (initializer) {
433 return new CodeGradX.State(initializer);
434};
435
436/** Get current user (if defined). This is particularly useful when
437 the user is not authenticated via getAuthenticatedUser() (for
438 instance, via GoogleOpenId).
439
440 @return {Promise<User>} yields {User}
441
442*/
443
444CodeGradX.getCurrentUser = function (force) {
445 const state = CodeGradX.getCurrentState();
446 if ( !force && state.currentUser ) {
447 return when(state.currentUser);
448 }
449 state.debug('getCurrentUser1');
450 let params = {};
451 const currentCampaignName = isCurrentCampaignDefined();
452 if ( currentCampaignName ) {
453 params.campaign = currentCampaignName;
454 }
455 return state.sendAXServer('x', {
456 path: '/whoami',
457 method: 'GET',
458 headers: {
459 'Accept': 'application/json'
460 },
461 entity: params
462 }).then(function (response) {
463 //console.log(response);
464 state.debug('getCurrentUser2', response);
465 state.currentUser = new CodeGradX.User(response.entity);
466 return when(state.currentUser);
467 });
468};
469
470
471/** Helper function, add a fact to the log held in the current state
472 {@see CodeGradX.Log.debug} documentation.
473
474 @returns {Log}
475
476*/
477
478CodeGradX.State.prototype.debug = function () {
479 return this.log.debug.apply(this.log, arguments);
480};
481
482/** Empty cache to gain room.
483*/
484
485CodeGradX.State.prototype.gc = function () {
486 const state = this;
487 state.cache.jobs = {};
488 // FUTURE remove also .exercises ....................
489};
490
491/** Update the description of a server in order to determine if that
492 server is available. The description may contain an optional `host`
493 key with the name of the host to be checked. If the name is missing,
494 the hostname is automatically inferred from the `kind`, `index` and
495 `domain` information. After the check, the `enabled` key is set to
496 a boolean telling wether the host is available or not.
497
498 Descriptions are gathered in `descriptions` with one additional key:
499 `suffix` is the path to add to the URL used to check the
500 availability of the server.
501
502 @param {string} kind - the kind of server (a, e, x or s)
503 @param {number} index - the index of the server.
504 @returns {Promise<Response>} - Promise leading to {HTTPresponse}
505
506 Descriptions are kept in the global state.
507 */
508
509CodeGradX.State.prototype.checkServer = function (kind, index) {
510 const state = this;
511 state.debug('checkServer1', kind, index);
512 if ( ! state.servers[kind] ) {
513 state.servers[kind] = {};
514 }
515 const descriptions = state.servers[kind];
516 if ( ! descriptions[index] ) {
517 descriptions[index] = { enabled: false };
518 }
519 const description = descriptions[index];
520 const host = description.host || (kind + index + state.servers.domain);
521 description.host = host;
522 description.protocol = description.protocol || descriptions.protocol;
523 // Don't use that host while being checked:
524 description.enabled = false;
525 delete description.lastError;
526 function updateDescription (response) {
527 state.debug('updateDescription', description.host, response);
528 description.enabled = (response.status.code < 300);
529 return when(response);
530 }
531 function invalidateDescription (reason) {
532 state.debug('invalidateDescription', description.host, reason);
533 description.enabled = false;
534 description.lastError = reason;
535 return when.reject(reason);
536 }
537 const url = description.protocol + "://" + host + descriptions.suffix;
538 state.debug('checkServer2', kind, index, url);
539 const request = {
540 path: url
541 };
542 if ( state.currentCookie ) {
543 if ( ! request.headers ) {
544 request.headers = {};
545 }
546 // To send this header imposes a pre-flight:
547 //if ( kind !== 's' ) {
548 // request.headers['X-FW4EX-Cookie'] = state.currentCookie;
549 //}
550 if ( CodeGradX.isNode() ) {
551 request.headers.Cookie = state.currentCookie;
552 } else {
553 if ( ! document.cookie.indexOf(state.currentCookie) ) {
554 document.cookie = state.currentCookie + ";path='/';";
555 }
556 }
557 }
558 if ( kind !== 's' ) {
559 request.mixin = {
560 withCredentials: true
561 };
562 }
563 return state.userAgent(request)
564 .then(updateDescription)
565 .catch(invalidateDescription);
566};
567
568/** Check all possible servers of some kind (a, e, x or s) that is,
569 update the state for those servers. If correctly programmed these
570 checks are concurrently run but `checkServers` will only be
571 resolved when all concurrent checks are resolved. However there is
572 a timeout of 3 seconds.
573
574 @param {string} kind - the kind of server (a, e, x or s)
575 @returns {Promise} yields Descriptions
576
577 Descriptions = { 0: Description, 1: Description, ... }
578 Description = { host: "", enabled: boolean, ... }
579
580 */
581
582CodeGradX.State.prototype.checkServers = function (kind) {
583 const state = this;
584 state.debug('checkServers', kind);
585 const descriptions = state.servers[kind];
586 let promise;
587 const promises = [];
588 for ( let key in descriptions ) {
589 if ( /^\d+$/.exec(key) ) {
590 key = CodeGradX._str2num(key);
591 promise = state.checkServer(kind, key);
592 promise = promise.timeout(CodeGradX.State.maxWait);
593 promises.push(promise);
594 }
595 }
596 function returnDescriptions () {
597 state.debug('returnDescriptions', descriptions);
598 return when(descriptions);
599 }
600 return when.settle(promises)
601 .then(returnDescriptions)
602 .catch(returnDescriptions);
603};
604CodeGradX.State.maxWait = 3000; // 3 seconds
605
606/** Filter out of the descriptions of some 'kind' of servers those
607 that are deemed to be available. If no availableserver is found
608 then check all servers.
609
610 @param {string} kind - the kind of server (a, e, x or s)
611 @returns {Promise} yielding Array[Description]
612
613 Descriptions = { 0: Description, 1: Description, ... }
614 Description = { host: "", enabled: boolean, ... }
615
616*/
617
618CodeGradX.State.prototype.getActiveServers = function (kind) {
619 const state = this;
620 const descriptions = state.servers[kind];
621 function filterDefined (array) {
622 const result = [];
623 array.forEach(function (item) {
624 if ( item ) {
625 result.push(item);
626 }
627 });
628 return result;
629 }
630 state.debug("getActiveServers Possible:", kind,
631 filterDefined(_.map(descriptions, 'host')));
632 // _.filter leaves 'undefined' values in the resulting array:
633 let active = filterDefined(_.filter(descriptions, {enabled: true}));
634 state.debug('getActiveServers Active:', kind,
635 _.map(active, 'host'));
636 if ( active.length === 0 ) {
637 // check again all servers:
638 return state.checkServers(kind)
639 .then(function (descriptions) {
640 active = filterDefined(_.filter(descriptions, {enabled: true}));
641 if ( active.length === 0 ) {
642 const error = new Error(`No available ${kind} servers`);
643 return when.reject(error);
644 } else {
645 return when(active);
646 }
647 });
648 } else {
649 return when(active);
650 }
651};
652
653/** Check HTTP response and try to elaborate a good error message.
654 A good HTTP response has a return code less than 300.
655
656 Error messages look like:
657 <?xml version="1.0" encoding="UTF-8"?>
658 <fw4ex version='1.0'>
659 <errorAnswer>
660 <message code='400'>
661 <reason>FW4EX e135 Not a tar gzipped file!</reason>
662 </message>
663 </errorAnswer>
664 </fw4ex>
665
666 */
667
668CodeGradX.checkStatusCode = function (response) {
669 const state = CodeGradX.getCurrentState();
670 state.debug('checkStatusCode1', response);
671 //console.log(response);
672 /* eslint no-control-regex: 0 */
673 const reasonRegExp = new RegExp("^(.|\n)*<reason>((.|\n)*)</reason>(.|\n)*$");
674 function extractFW4EXerrorMessage (response) {
675 let reason;
676 const contentType = response.headers['Content-Type'];
677 if ( /text\/xml/.exec(contentType) ) {
678 //console.log(response.entity);
679 reason = response.entity.replace(reasonRegExp, ": $2");
680 return reason;
681 } else if ( /application\/json/.exec(contentType) ) {
682 reason = response.entity.reason;
683 return reason;
684 } else {
685 return '';
686 }
687 }
688 if ( response.status &&
689 response.status.code &&
690 response.status.code >= 300 ) {
691 const msg = "Bad HTTP code " + response.status.code + ' ' +
692 extractFW4EXerrorMessage(response);
693 state.debug('checkStatusCode2', msg);
694 //console.log(response);
695 const error = new Error(msg);
696 error.response = response;
697 return when.reject(error);
698 }
699 return when(response);
700};
701
702/** Send request to the first available server of the right kind.
703 In case of problems, try sequentially the next available server of
704 the same kind.
705
706 @param {string} kind - the kind of server (usually a or x)
707 @param {object} options - description of the HTTP request to send
708 @property {string} options.path
709 @property {string} options.method
710 @property {object} options.headers - for instance Accept, Content-Type
711 @property {object} options.entity - string or object depending on Content-Type
712 @returns {Promise} yields {HTTPresponse}
713
714 */
715
716CodeGradX.State.prototype.sendSequentially = function (kind, options) {
717 const state = this;
718 state.debug('sendSequentially', kind, options);
719
720 function regenerateNewOptions (options) {
721 const newoptions = Object.assign({}, options);
722 newoptions.headers = newoptions.headers || options.headers || {};
723 if ( state.currentCookie ) {
724 //newoptions.headers['X-FW4EX-Cookie'] = state.currentCookie;
725 if ( CodeGradX.isNode() ) {
726 newoptions.headers.Cookie = state.currentCookie;
727 } else {
728 if ( ! document.cookie.indexOf(state.currentCookie) ) {
729 document.cookie = state.currentCookie + ";path='/';";
730 }
731 }
732 }
733 return newoptions;
734 }
735
736 function updateCurrentCookie (response) {
737 //console.log(response.headers);
738 //console.log(response);
739 state.debug('sendSequentially updateCurrentCookie', response);
740 function extractCookie (tag) {
741 if ( response.headers[tag] ) { // char case ?
742 let cookies = response.headers[tag];
743 cookies = _.map(cookies, function (s) {
744 return s.replace(/;.*$/, '');
745 });
746 cookies = _.filter(cookies, function (s) {
747 s = s.replace(/^u=/, '');
748 return /^U/.exec(s);
749 });
750 return (state.currentCookie = cookies);
751 }
752 }
753 if ( ! extractCookie('Set-Cookie') ) {
754 extractCookie('X-CodeGradX-Cookie');
755 }
756 return when(response);
757 }
758
759 function mk_invalidate (description) {
760 // This function declares the host as unable to answer.
761 // Meanwhile, the host may answer with bad status code!
762 return function (reason) {
763 state.debug('sendAXserver invalidate', description, reason);
764 //console.log(reason);
765 description.enabled = false;
766 description.lastError = reason;
767 return when.reject(reason);
768 };
769 }
770 function send (description) {
771 const newoptions = regenerateNewOptions(options);
772 newoptions.protocol = newoptions.protocol || description.protocol;
773 newoptions.path = newoptions.protocol + '://' +
774 description.host + options.path;
775 newoptions.mixin = {
776 withCredentials: true
777 };
778 state.debug('sendSequentially send', newoptions.path);
779 return state.userAgent(newoptions)
780 .catch(mk_invalidate(description))
781 .then(CodeGradX.checkStatusCode)
782 .then(updateCurrentCookie);
783 }
784
785 function trySequentially (adescriptions) {
786 let promise = when.reject('start');
787 adescriptions.forEach(function (description) {
788 promise = promise.catch(function (reason) {
789 state.debug('sendSequentially trySequentially', reason);
790 return send(description);
791 });
792 });
793 return promise;
794 }
795 function retrySequentially (reason) {
796 state.debug('sendSequentially retry', reason);
797 return state.getActiveServers(kind)
798 .then(trySequentially);
799 }
800
801 return state.getActiveServers(kind)
802 .then(trySequentially)
803 .catch(retrySequentially);
804};
805
806/** By default sending to an A or X server is done sequentially until
807 one answers positively.
808*/
809
810CodeGradX.State.prototype.sendAXServer = function (kind, options) {
811 const state = this;
812 return state.sendSequentially(kind, options);
813};
814
815/** Send request concurrently to all available servers. The fastest wins.
816
817 @param {string} kind - the kind of server (usually e or s)
818 @param {object} options - description of the HTTP request to send
819 @property {string} woptions.path
820 @property {string} options.method
821 @property {object} options.headers - for instance Accept, Content-Type
822 @property {object} options.entity - string or object depending on Content-Type
823 @returns {Promise} yields {HTTPresponse}
824
825*/
826
827
828CodeGradX.State.prototype.sendConcurrently = function (kind, options) {
829 const state = this;
830 state.debug('sendConcurrently', kind, options);
831
832 function regenerateNewOptions (options) {
833 const newoptions = Object.assign({}, options);
834 newoptions.headers = newoptions.headers || options.headers || {};
835 if ( state.currentCookie ) {
836 //newoptions.headers['X-FW4EX-Cookie'] = state.currentCookie;
837 if ( CodeGradX.isNode() ) {
838 newoptions.headers.Cookie = state.currentCookie;
839 } else {
840 if ( ! document.cookie.indexOf(state.currentCookie) ) {
841 document.cookie = state.currentCookie + ";path='/';";
842 }
843 }
844 }
845 if ( kind === 'e' ) {
846 newoptions.mixin = {
847 withCredentials: true
848 };
849 }
850 return newoptions;
851 }
852
853 function mk_invalidate (description) {
854 return function seeError (reason) {
855 // A MIME deserialization problem may also trigger `seeError`.
856 function see (o) {
857 let result = '';
858 for ( let key in o ) {
859 result += key + '=' + o[key] + ' ';
860 }
861 return result;
862 }
863 state.debug('sendConcurrently seeError', see(reason));
864 // Don't consider the absence of a report to be a
865 // reason to disable the server.
866 description.enabled = false;
867 description.lastError = reason;
868 //const js = JSON.parse(reason.entity);
869 return when.reject(reason);
870 };
871 }
872
873 function send (description) {
874 const tryoptions = Object.assign({}, regenerateNewOptions(options));
875 tryoptions.path = description.protocol + '://' +
876 description.host + options.path;
877 state.debug("sendConcurrently send", tryoptions.path);
878 return state.userAgent(tryoptions)
879 .catch(mk_invalidate(description))
880 .then(CodeGradX.checkStatusCode);
881 }
882
883 function tryConcurrently (adescriptions) {
884 const promises = adescriptions.map(send);
885 return when.any(promises);
886 }
887
888 return state.getActiveServers(kind)
889 .then(tryConcurrently);
890};
891
892/** By default requesting an E or S server is done concurrently (except
893 when submitting a new exercise).
894*/
895
896CodeGradX.State.prototype.sendESServer = function (kind, options) {
897 const state = this;
898 return state.sendConcurrently(kind, options);
899};
900
901/** Ask repeatedly an E or S server.
902 Send request to all available servers and repeat in case of problems.
903
904 @param {Object} parameters -
905 @property {number} parameters.step - seconds between each attempt
906 @property {number} parameters.attempts - at most n attempts
907 @property {function} parameters.progress -
908 @returns {Promise} yields {HTTPresponse}
909
910 The `progress` function (parameters) {} is invoked before each attempt.
911 By default, `parameters` is initialized with
912 CodeGradX.State.prototype.sendRepeatedlyESServer.default
913
914 If a server has a 'once' property, it must be asked only once.
915
916 Nota: when.any does not cancel the other concurrent promises.
917 */
918
919CodeGradX.State.prototype.sendRepeatedlyESServer =
920function (kind, parameters, options) {
921 const state = this;
922 state.debug('sendRepeatedlyESServer', kind, parameters, options);
923 const parms = Object.assign({ i: 0 },
924 CodeGradX.State.prototype.sendRepeatedlyESServer.default,
925 parameters);
926 let count = parms.attempts;
927
928 function removeOnceServers (adescriptions) {
929 const aresult = [];
930 for (let item of adescriptions) {
931 if ( ! item.once ) {
932 aresult.push(item);
933 }
934 }
935 state.debug('sendRepeatedlyESServer Non Once active servers',
936 kind, _.map(aresult, 'host'));
937 return aresult;
938 }
939 function retry (reason) {
940 state.debug('sendRepeatedlyESServer retry', reason, count--);
941 try {
942 parms.progress(parms);
943 } catch (exc) {
944 state.debug('sendRepeatedlyESServer progress', exc);
945 }
946 if ( count <= 0 ) {
947 return when.reject(new Error("waitedTooMuch"));
948 }
949 return state.getActiveServers(kind)
950 .then(removeOnceServers)
951 .delay(parms.step * 1000)
952 .then(function () {
953 return state.sendESServer(kind, options);
954 })
955 .catch(retry);
956 }
957
958 return state.sendESServer(kind, options)
959 .catch(retry);
960};
961CodeGradX.State.prototype.sendRepeatedlyESServer.default = {
962 step: 3, // seconds
963 attempts: 30,
964 progress: function (/*parameters*/) {}
965};
966
967/** Authenticate the user. This will return a Promise leading to
968 some User.
969
970 @param {string} login - real login or email address
971 @param {string} password
972 @returns {Promise<User>} yields {User}
973
974 */
975
976CodeGradX.State.prototype.getAuthenticatedUser =
977function (login, password) {
978 const state = this;
979 state.debug('getAuthenticatedUser1', login);
980 return state.sendAXServer('x', {
981 path: '/direct/check',
982 method: 'POST',
983 headers: {
984 'Accept': 'application/json',
985 'Content-Type': 'application/x-www-form-urlencoded'
986 },
987 entity: {
988 login: login,
989 password: password
990 }
991 }).then(function (response) {
992 //console.log(response);
993 state.debug('getAuthenticatedUser2', response);
994 state.currentUser = new CodeGradX.User(response.entity);
995 return when(state.currentUser);
996 });
997};
998
999/** Disconnect the user.
1000
1001 @returns {Promise<>} yields undefined
1002
1003*/
1004
1005CodeGradX.State.prototype.userDisconnect = function () {
1006 var state = this;
1007 state.debug('userDisconnect1');
1008 return state.sendAXServer('x', {
1009 path: '/fromp/disconnect',
1010 method: 'GET',
1011 headers: {
1012 'Accept': 'application/json',
1013 'Content-Type': 'application/x-www-form-urlencoded'
1014 }
1015 }).then(function (response) {
1016 //console.log(response);
1017 state.debug('userDisconnect2', response);
1018 state.currentUser = undefined;
1019 return when(undefined);
1020 });
1021};
1022
1023// **************** User *******************************
1024
1025/** Represents a User. An User is found by its login and password, the login
1026 may be a real login (such as upmc:1234567) or an email address.
1027
1028 @constructor
1029 @property {string} lastname
1030 @property {string} firstname
1031 @property {string} email
1032 @property {string} cookie
1033 @property {number} personid
1034 @property {string} pseudo
1035 @property {Array<string>} authorprefixes
1036 @property {Hashtable<Campaign>} _campaigns - Hashtable of current Campaigns
1037 @property {Hashtable<Campaign>} _all_campaigns - Hashtable of all Campaigns
1038
1039 Campaigns may be obtained via `getCampaign()` or `getCampaigns()`.
1040
1041 */
1042
1043CodeGradX.User = function (json) {
1044 Object.assign(this, json);
1045 //console.log(json);
1046 delete this.kind;
1047 const state = CodeGradX.getCurrentState();
1048 if ( this.cookie ) {
1049 if ( ! state.currentCookie ) {
1050 state.currentCookie = this.cookie;
1051 }
1052 } else if ( state.currentCookie ) {
1053 this.cookie = state.currentCookie;
1054 }
1055 if ( _.has(json, 'campaigns') ) {
1056 const campaigns = {};
1057 json.campaigns.forEach(function (js) {
1058 //console.log(js);
1059 const campaign = new CodeGradX.Campaign(js);
1060 campaigns[campaign.name] = campaign;
1061 });
1062 // Just record the current active campaigns:
1063 this._campaigns = campaigns;
1064 }
1065};
1066
1067/** Modify some properties of the current user. These properties are
1068
1069 @param {object} fields
1070 @property {string} fields.lastname
1071 @property {string} fields.firstname
1072 @property {string} fields.pseudo
1073 @property {string} fields.email
1074 @property {string} fields.password
1075 @returns {Promise yields User
1076
1077 It is not possible to change user's login, personid, authorprefixes.
1078
1079 */
1080
1081CodeGradX.User.prototype.modify = function (fields) {
1082 const state = CodeGradX.getCurrentState();
1083 state.debug('modify1', fields);
1084 return state.sendAXServer('x', {
1085 path: '/person/selfmodify',
1086 method: 'POST',
1087 headers: {
1088 'Accept': 'application/json',
1089 'Content-Type': 'application/x-www-form-urlencoded'
1090 },
1091 entity: fields
1092 }).then(function (response) {
1093 state.debug('modify2', response);
1094 delete response.entity.kind;
1095 CodeGradX.User.call(state.currentUser, response.entity);
1096 return when(state.currentUser);
1097 });
1098};
1099
1100/** Get the campaigns where the current user is enrolled.
1101
1102 @param {bool} now - if true get only active campaigns.
1103 @returns {Promise<Hashtable<Campaign>>} yielding a Hashtable of Campaigns
1104 indexed by their name.
1105
1106 The current user maintains in _campaigns the active campaigns and
1107 in _all_campaigns all past or current campaigns. Three cases are
1108 possible:
1109 - both are defined
1110 - only _campaigns is defined (see constructor 'User')
1111 - none are defined
1112
1113 */
1114
1115CodeGradX.User.prototype.getCampaigns = function (now) {
1116 const user = this;
1117 function filterActive (campaigns) {
1118 const activeCampaigns = {};
1119 _.forEach(campaigns, function (campaign) {
1120 if ( campaign.active ) {
1121 activeCampaigns[campaign.name] = campaign;
1122 }
1123 });
1124 return activeCampaigns;
1125 }
1126 if ( now ) {
1127 if ( user._campaigns ) {
1128 // return all current campaigns:
1129 return when(user._campaigns);
1130 } else if ( user._all_campaigns ) {
1131 // generate all current campaigns:
1132 user._campaigns = filterActive(user._all_campaigns);
1133 return when(user._campaigns);
1134 }
1135 }
1136 if ( user._all_campaigns ) {
1137 if ( now ) {
1138 user._campaigns = filterActive(user._all_campaigns);
1139 return when(user._campaigns);
1140 } else {
1141 return when(user._all_campaigns);
1142 }
1143 } else {
1144 const state = CodeGradX.getCurrentState();
1145 state.debug('getAllCampaigns1');
1146 return state.sendAXServer('x', {
1147 path: '/campaigns/',
1148 method: 'GET',
1149 headers: {
1150 'Accept': 'application/json',
1151 'Content-Type': 'application/x-www-form-urlencoded'
1152 }
1153 }).then(function (response) {
1154 state.debug('getAllCampaigns2', response);
1155 const campaigns = {};
1156 response.entity.forEach(function (js) {
1157 //console.log(js);
1158 const campaign = new CodeGradX.Campaign(js);
1159 campaigns[campaign.name] = campaign;
1160 });
1161 user._all_campaigns = campaigns;
1162 user._campaigns = filterActive(user._all_campaigns);
1163 if ( now ) {
1164 return when(user._campaigns);
1165 } else {
1166 return when(user._all_campaigns);
1167 }
1168 });
1169 }
1170};
1171
1172/** Return a specific Campaign. It looks for a named campaign among
1173 the campaigns the user is part of whether past or current.
1174
1175 @param {String} name - name of the Campaign to find
1176 @returns {Promise<Campaign>} yields {Campaign}
1177
1178 */
1179
1180CodeGradX.User.prototype.getCampaign = function (name) {
1181 const user = this;
1182 const state = CodeGradX.getCurrentState();
1183 state.debug('getCampaign', name);
1184 if ( user._campaigns && user._campaigns[name] ) {
1185 return when(user._campaigns[name]);
1186 } else if ( user._all_campaigns && user._all_campaigns[name] ) {
1187 return when(user._all_campaigns[name]);
1188 } else {
1189 return user.getCampaigns()
1190 .then(function (campaigns) {
1191 if ( campaigns && campaigns[name] ) {
1192 return when(campaigns[name]);
1193 } else {
1194 return when.reject(new Error("No such campaign " + name));
1195 }
1196 });
1197 }
1198};
1199
1200/** Get current campaign if FW4EX.currentCampaignName is defined or
1201 if there is a single active campaign associated to the user.
1202
1203 @return {Promise<Campaign>} yields {Campaign}
1204
1205 FUTURE: remove that dependency against FW4EX!!!!!!!!!!!!
1206*/
1207
1208function isCurrentCampaignDefined () {
1209 return CodeGradX.User.prototype.getCurrentCampaign.default.currentCampaign;
1210}
1211
1212CodeGradX.User.prototype.getCurrentCampaign = function () {
1213 const user = this;
1214 const currentCampaignName = isCurrentCampaignDefined();
1215 if ( currentCampaignName ) {
1216 return user.getCampaign(currentCampaignName)
1217 .then(function (campaign) {
1218 FW4EX.currentCampaign = campaign;
1219 return when(campaign);
1220 });
1221 } else {
1222 return user.getCampaigns(true)
1223 .then(function (campaigns) {
1224 function hash2array (o) {
1225 let result = [];
1226 Object.keys(o).forEach((key) => {
1227 result.push(o[key]);
1228 });
1229 return result;
1230 }
1231 campaigns = hash2array(campaigns);
1232 if ( campaigns.length === 1 ) {
1233 FW4EX.currentCampaignName = campaigns[0].name;
1234 FW4EX.currentCampaign = campaigns[0];
1235 return when(campaigns[0]);
1236 } else if ( FW4EX.currentCampaign ) {
1237 return when(FW4EX.currentCampaign);
1238 } else {
1239 const msg = "Cannot determine current campaign";
1240 return when.reject(new Error(msg));
1241 }
1242 });
1243 }
1244};
1245CodeGradX.User.prototype.getCurrentCampaign.default = {
1246 currentCampaign: undefined
1247};
1248
1249/** Fetch all the jobs submitted by the user (independently of the
1250 current campaign).
1251
1252 @returns {Promise<Jobs>} yields {Array[Job]}
1253
1254 */
1255
1256CodeGradX.User.prototype.getAllJobs = function () {
1257 const state = CodeGradX.getCurrentState();
1258 const user = this;
1259 state.debug('getAllJobs1', user);
1260 return state.sendAXServer('x', {
1261 path: '/history/jobs',
1262 method: 'GET',
1263 headers: {
1264 Accept: "application/json"
1265 }
1266 }).then(function (response) {
1267 state.debug('getAllJobs2');
1268 //console.log(response);
1269 state.jobs = _.map(response.entity.jobs, CodeGradX.Job.js2job);
1270 return when(state.jobs);
1271 });
1272};
1273
1274/** Fetch all exercises submitted by the user (independently of the
1275 current campaign) but only the exercices created after the
1276 starttime of the current campaign.
1277
1278 @returns {Promise<Exercises>} yields {Array[Exercise]}
1279
1280 */
1281
1282CodeGradX.User.prototype.getAllExercises = function () {
1283 const state = CodeGradX.getCurrentState();
1284 const user = this;
1285 state.debug('getAllExercises1', user);
1286 return CodeGradX.getCurrentUser()
1287 .then(function (user) {
1288 return user.getCurrentCampaign();
1289 }).then(function (campaign) {
1290 FW4EX.fillCampaignCharacteristics(campaign);
1291 let url = `/exercises/person/${user.personid}`;
1292 let d = campaign.starttime.toISOString().replace(/T.*$/, '');
1293 url += `?after=${encodeURI(d)}`;
1294 return state.sendAXServer('x', {
1295 path: url,
1296 method: 'GET',
1297 headers: {
1298 Accept: "application/json",
1299 "Content-Type": "application/json"
1300 }
1301 });
1302 }).then(function (response) {
1303 state.debug('getAllExercises2');
1304 //console.log(response);
1305 state.exercises = _.map(response.entity.exercises,
1306 CodeGradX.Exercise.js2exercise);
1307 return when(state.exercises);
1308 });
1309};
1310
1311/** get the list of exercises a user tried in a given campaign, get
1312 also the list of badges (or certificates) won during that
1313 campaign. It enriches the current user with new properties
1314 results and badges.
1315
1316 @param {Campaign} campaign - Campaign
1317 @return {Promise<User>} yielding the User
1318 @property {array[string]} user.badges - urls of badges
1319 @property {number} user.results[].mark - gotten mark
1320 @property {string} user.results[].name - exercise long name
1321 @property {string} user.results[].nickname - exercise nickname
1322
1323 */
1324
1325CodeGradX.User.prototype.getProgress = function (campaign) {
1326 const state = CodeGradX.getCurrentState();
1327 const user = this;
1328 state.debug('getProgress1', user);
1329 return state.sendAXServer('x', {
1330 path: ('/skill/progress/' + campaign.name),
1331 method: 'GET',
1332 headers: {
1333 Accept: "application/json"
1334 }
1335 }).then(function (response) {
1336 state.debug('getProgress2', response);
1337 //console.log(response);
1338 user.results = response.entity.results;
1339 user.badges = response.entity.badges;
1340 return when(user);
1341 });
1342};
1343
1344/** submit a new Exercise and return it as soon as submitted successfully.
1345 This variant sends the content of a DOM form.
1346
1347 @param {DOM} form - a DOM element
1348 @returns {Promise<Exercise>} yielding Exercise
1349
1350 */
1351
1352CodeGradX.User.prototype.submitNewExerciseFromDOM = function (form) {
1353 const user = this;
1354 const state = CodeGradX.getCurrentState();
1355 state.debug('submitNewExerciseFromDOM1', user);
1356 function processResponse (response) {
1357 //console.log(response);
1358 state.debug('submitNewExerciseFromDOM3', response);
1359 return CodeGradX.parsexml(response.entity).then(function (js) {
1360 //console.log(js);
1361 state.debug('submitNewExerciseFromDOM4', js);
1362 js = js.fw4ex.exerciseSubmittedReport;
1363 const exercise = new CodeGradX.Exercise({
1364 location: js.$.location,
1365 personid: CodeGradX._str2num(js.person.$.personid),
1366 exerciseid: js.exercise.$.exerciseid,
1367 XMLsubmission: response.entity
1368 });
1369 state.debug('submitNewExerciseFromDOM5', exercise.exerciseid);
1370 return when(exercise);
1371 });
1372 }
1373 const fd = new FormData(form);
1374 const basefilename = FW4EX.currentFileName
1375 .replace(new RegExp("^.*/"), '');
1376 const headers = {
1377 "Content-Type": "multipart/form-data",
1378 "Content-Disposition": ("inline; filename=" + basefilename),
1379 "Accept": 'text/xml'
1380 };
1381 return state.sendSequentially('e', {
1382 path: '/exercises/',
1383 method: "POST",
1384 headers: headers,
1385 entity: fd
1386 }).then(processResponse);
1387};
1388
1389/** Disconnect the user.
1390
1391 @returns {Promise<User>} yields {User}
1392
1393*/
1394
1395CodeGradX.User.prototype.disconnect = function () {
1396 return CodeGradX.getCurrentState().userDisconnect();
1397};
1398
1399// **************** Campaign *********************************
1400
1401/** A campaign describes a set of exercises for a given group of
1402 students and a given group of teachers for a given period of time.
1403 These groups of persons are not public.
1404
1405 @constructor
1406 @property {string} name
1407 @property {Date} starttime - Start date of the Campaign
1408 @property {Date} endtime - End date of the Campaign
1409 @property {ExerciseSet} _exercises (filled by getExercises)
1410
1411 Exercises may be obtained one by one with `getExercise()`.
1412
1413 */
1414
1415CodeGradX.Campaign = function (json) {
1416 // initialize name, starttime, endtime
1417 Object.assign(this, json);
1418 this.starttime = CodeGradX._str2Date(json.starttime);
1419 this.endtime = CodeGradX._str2Date(json.endtime);
1420 //console.log(this);
1421};
1422
1423/** Get the list of all students enrolled in the current campaign.
1424
1425 @return {Promise<Array[Object]>} - yield an array of students
1426 @property {string} student.lastname
1427 @property {string} student.firstname
1428 @property {string} student.pseudo
1429 @property {string} student.email
1430 @property {bool} student.confirmedemail
1431 @property {number} student.confirmedua
1432 @property {start} student.start - creation date
1433
1434 */
1435
1436CodeGradX.Campaign.prototype.getStudents = function (refresh = false) {
1437 const state = CodeGradX.getCurrentState();
1438 const campaign = this;
1439 state.debug('getStudents1', campaign);
1440 if ( ! refresh && campaign.students ) {
1441 return when(campaign.students);
1442 }
1443 return state.sendAXServer('x', {
1444 path: ('/campaign/listStudents/' + campaign.name),
1445 method: 'GET',
1446 headers: {
1447 Accept: "application/json"
1448 }
1449 }).then(function (response) {
1450 state.debug('getStudents2');
1451 //console.log(response);
1452 campaign.students = response.entity.students.map(function (student) {
1453 return new CodeGradX.User(student);
1454 });
1455 return when(campaign.students);
1456 });
1457};
1458
1459/** Get the list of all teachers enrolled in the current campaign.
1460
1461 @return {Promise<Array[Object]>} - yield an array of teachers
1462 @property {string} teacher.lastname
1463 @property {string} teacher.firstname
1464 @property {string} teacher.pseudo
1465 @property {string} teacher.email
1466 @property {bool} teacher.confirmedemail
1467 @property {number} teacher.confirmedua
1468
1469 */
1470
1471CodeGradX.Campaign.prototype.getTeachers = function (refresh) {
1472 const state = CodeGradX.getCurrentState();
1473 const campaign = this;
1474 state.debug('getTeachers1', campaign);
1475 if ( ! refresh && campaign.teachers ) {
1476 return when(campaign.teachers);
1477 }
1478 return state.sendAXServer('x', {
1479 path: ('/campaign/listTeachers/' + campaign.name),
1480 method: 'GET',
1481 headers: {
1482 Accept: "application/json"
1483 }
1484 }).then(function (response) {
1485 state.debug('getTeachers2');
1486 //console.log(response);
1487 campaign.teachers = response.entity.teachers.map(function (teacher) {
1488 let tuser = new CodeGradX.User(teacher);
1489 // Don't duplicate the requester's cookie:
1490 delete tuser.cookie;
1491 return tuser;
1492 });
1493 return when(campaign.teachers);
1494 });
1495};
1496
1497/** Promote a student to a teacher position.
1498
1499 @return {Promise<Array[Object]>} - yield an array of teachers
1500 @param {User} student - the student to promote
1501
1502*/
1503
1504CodeGradX.Campaign.prototype.promoteStudent = function (student) {
1505 const state = CodeGradX.getCurrentState();
1506 const campaign = this;
1507 state.debug('promoteTeacher1', campaign, student.personid);
1508 return state.sendAXServer('x', {
1509 path: ('/campaign/promote/' + campaign.name + '/' + student.personid),
1510 method: 'POST',
1511 headers: {
1512 Accept: "application/json"
1513 }
1514 }).then(function (response) {
1515 state.debug('promoteTeacher2');
1516 //console.log(response);
1517 campaign.teachers = response.entity.teachers.map(function (teacher) {
1518 let tuser = new CodeGradX.User(teacher);
1519 // Don't duplicate the requester's cookie:
1520 delete tuser.cookie;
1521 return tuser;
1522 });
1523 return when(campaign.teachers);
1524 });
1525};
1526
1527/** Demote a teacher into a student position or
1528 remove a student from the group of students
1529
1530 @return {Promise<Array[Object]>} - yield an array of teachers
1531 @param {User} student - the student to promote
1532
1533*/
1534
1535CodeGradX.Campaign.prototype.demoteTeacher = function (student) {
1536 const state = CodeGradX.getCurrentState();
1537 const campaign = this;
1538 state.debug('demoteTeacher1', campaign, student.personid);
1539 return state.sendAXServer('x', {
1540 path: ('/campaign/demote/' + campaign.name + '/' + student.personid),
1541 method: 'POST',
1542 headers: {
1543 Accept: "application/json"
1544 }
1545 }).then(function (response) {
1546 state.debug('demoteTeacher2');
1547 //console.log(response);
1548 return when(response.entity);
1549 });
1550};
1551
1552/** Get the list of all exercises available in the current campaign.
1553 The user must be a teacher of the campaign!
1554
1555 @return {Promise<Array[Object]>} - yield an array of exercises
1556 @property {string} exercise.nickname
1557 @property {string} exercise.name
1558 @property {string} exercise.UUID
1559 @property {date} exercise.start
1560
1561 */
1562
1563CodeGradX.Campaign.prototype.getExercises = function (refresh = false) {
1564 const state = CodeGradX.getCurrentState();
1565 const campaign = this;
1566 state.debug('getExercises1', campaign);
1567 if ( ! refresh && campaign.exercises ) {
1568 return when(campaign.exercises);
1569 }
1570 return state.sendAXServer('x', {
1571 path: ('/campaign/listExercises/' + campaign.name),
1572 method: 'GET',
1573 headers: {
1574 Accept: "application/json"
1575 }
1576 }).then(function (response) {
1577 state.debug('getExercises2');
1578 //console.log(response);
1579 campaign.exercises = response.entity.exercises.map(function (exercise) {
1580 return new CodeGradX.Exercise(exercise);
1581 });
1582 return when(campaign.exercises);
1583 });
1584};
1585
1586/** Get the list of all batches related to a campaign.
1587
1588 @return {Promise<Array[Object]>} - yield an array of batches
1589 @property {string} batch.uuid
1590 @property {Exercise} batch.exercise_uuid
1591 @property {Person} batch.person_id
1592 @property {string} batch.label
1593 @property {Date} batch.start
1594 @property {Date} batch.archived
1595 @property {Date} batch.finished
1596
1597 */
1598
1599CodeGradX.Campaign.prototype.getBatches = function (refresh = false) {
1600 const state = CodeGradX.getCurrentState();
1601 const campaign = this;
1602 state.debug('getBatches1', campaign);
1603 if ( ! refresh && campaign.batches ) {
1604 return when(campaign.batches);
1605 }
1606 return state.sendAXServer('x', {
1607 path: ('/campaign/listBatches/' + campaign.name),
1608 method: 'GET',
1609 headers: {
1610 Accept: "application/json"
1611 }
1612 }).then(function (response) {
1613 state.debug('getBatches2');
1614 //console.log(response);
1615 campaign.batches = response.entity.batches.map(function (batch) {
1616 return new CodeGradX.Batch(batch);
1617 });
1618 return when(campaign.batches);
1619 });
1620};
1621CodeGradX.Campaign.prototype.getBatchs =
1622 CodeGradX.Campaign.prototype.getBatches;
1623
1624
1625/** Get the skills of the students enrolled in the current campaign.
1626
1627 @return {Promise} yields {Object}
1628 @property {Object} skills.you
1629 @property {number} skills.you.personId - your numeric identifier
1630 @property {number} skills.you.skill - your own skill
1631 @property {Array<skill>} skills.all - array of Object
1632 @property {Object} skills.all[].skill - some student's skill
1633
1634 */
1635
1636CodeGradX.Campaign.prototype.getSkills = function (refresh = false) {
1637 const state = CodeGradX.getCurrentState();
1638 const campaign = this;
1639 state.debug('getSkills1', campaign);
1640 if ( ! refresh && campaign.skills ) {
1641 return when(campaign.skills);
1642 }
1643 return state.sendAXServer('x', {
1644 //path: ('/skill/campaign/' + campaign.name),
1645 path: ('/statistics/myPosition/' + campaign.name),
1646 method: 'GET',
1647 headers: {
1648 Accept: "application/json"
1649 }
1650 }).then(function (response) {
1651 state.debug('getSkills2');
1652 //console.log(response);
1653 campaign.skills = response.entity;
1654 return when(campaign.skills);
1655 });
1656};
1657
1658/** list the jobs submitted by the current user in the current campaign.
1659
1660 @returns {Promise} yields Array[Job]
1661 */
1662
1663CodeGradX.Campaign.prototype.getJobs = function () {
1664 const state = CodeGradX.getCurrentState();
1665 const campaign = this;
1666 state.debug('getJobs1', campaign, state.currentUser);
1667 return state.sendAXServer('x', {
1668 path: ('/history/campaign/' + campaign.name),
1669 method: 'GET',
1670 headers: {
1671 Accept: "application/json"
1672 }
1673 }).then(function (response) {
1674 state.debug('getJobs2');
1675 //console.log(response);
1676 state.jobs = _.map(response.entity.jobs, CodeGradX.Job.js2job);
1677 return when(state.jobs);
1678 });
1679};
1680
1681/** Get the jobs submitted by a student in the current campaign.
1682 This is restricted to admins or teachers of the campaign.
1683
1684 @returns {Promise} yields Array[Job]
1685*/
1686
1687CodeGradX.Campaign.prototype.getCampaignStudentJobs = function (user) {
1688 const state = CodeGradX.getCurrentState();
1689 const campaign = this;
1690 state.debug('getAchievements1', campaign, user);
1691 return state.sendAXServer('x', {
1692 path: ('/history/campaignJobs/' + campaign.name + '/' + user.personid),
1693 method: 'GET',
1694 headers: {
1695 Accept: "application/json"
1696 }
1697 }).then(function (response) {
1698 state.debug('getAchievements2');
1699 //console.log(response);
1700 user.jobs = _.map(response.entity.jobs, CodeGradX.Job.js2job);
1701 return when(user.jobs);
1702 });
1703};
1704
1705/** Get the (tree-shaped) set of exercises of a campaign. This
1706 mechanism is used to get an updated list of exercises. First, look
1707 in an X server then on the site associated to the campaign.
1708
1709 @return {Promise} yields {ExercisesSet}
1710
1711 */
1712
1713CodeGradX.Campaign.prototype.getExercisesSet = function () {
1714 const state = CodeGradX.getCurrentState();
1715 const campaign = this;
1716 state.debug('getExercisesSet1', campaign);
1717 if ( campaign.exercisesSet ) {
1718 return when(campaign.exercisesSet);
1719 }
1720 function processResponse (response) {
1721 state.debug('getExercisesSet1', response);
1722 campaign.exercisesSet = new CodeGradX.ExercisesSet(response.entity);
1723 return when(campaign.exercisesSet);
1724 }
1725
1726 const p3 = state.sendConcurrently('x', {
1727 path: ('/exercisesset/path/' + campaign.name),
1728 method: 'GET',
1729 headers: {
1730 Accept: "application/json"
1731 }
1732 });
1733 return p3.then(processResponse).catch(function (reason) {
1734 try {
1735 state.debug("getExercisesSet2Error", reason);
1736 const request1 = {
1737 method: 'GET',
1738 path: campaign.home_url + "/exercises.json",
1739 headers: {
1740 Accept: "application/json"
1741 }
1742 };
1743 return state.userAgent(request1)
1744 .then(processResponse);
1745 } catch (e) {
1746 // Probably: bad host name!
1747 state.debug("getExercisesSet3Error", e);
1748 }
1749 });
1750};
1751
1752/** Get a specific Exercise with its name within the tree of
1753 Exercises of the current campaign.
1754
1755 @param {string} name - full name of the exercise
1756 @returns {Promise} yields {Exercise}
1757
1758 */
1759
1760CodeGradX.Campaign.prototype.getExercise = function (name) {
1761 const state = CodeGradX.getCurrentState();
1762 state.debug('getExercise', name);
1763 const campaign = this;
1764 return campaign.getExercisesSet().then(function (exercisesSet) {
1765 const exercise = exercisesSet.getExercise(name);
1766 if ( exercise ) {
1767 return when(exercise);
1768 } else {
1769 return when.reject(new Error("No such exercise " + name));
1770 }
1771 });
1772};
1773
1774/** Send the content of a file selected by an input:file widget in the
1775 * browser.
1776
1777 @param {DOM} form DOM element
1778 @returns {Promise<ExercisesSet>} yields {ExercisesSet}
1779
1780The form DOM element must contain an <input type='file' name='content'>
1781element. This code only runs in a browser providing the FormData class.
1782
1783*/
1784
1785CodeGradX.Campaign.prototype.uploadExercisesSetFromDOM = function (form) {
1786 const state = CodeGradX.getCurrentState();
1787 const campaign = this;
1788 state.debug('uploadExercisesSetFromDOM1', FW4EX.currentExercisesSetFileName);
1789 function processResponse (response) {
1790 //console.log(response);
1791 state.debug('uploadExercisesSetFromDOM2', response);
1792 campaign.exercisesSet = new CodeGradX.ExercisesSet(response.entity);
1793 return when(campaign.exercisesSet);
1794 }
1795 const basefilename = FW4EX.currentFileName
1796 .replace(new RegExp("^.*/"), '');
1797 const headers = {
1798 "Content-Type": "multipart/form-data",
1799 "Content-Disposition": ("inline; filename=" + basefilename),
1800 "Accept": 'application/json'
1801 };
1802 const fd = new FormData(form);
1803 return state.sendAXServer('x', {
1804 path: ('/exercisesset/yml2json/' + campaign.name),
1805 method: "POST",
1806 headers: headers,
1807 entity: fd
1808 }).then(processResponse);
1809};
1810
1811/** Get related notifications.
1812
1813 @param {int} count - only last count notifications.
1814 @param {int} from - only notifications that occur in the last from hours
1815 @returns {Promise<Notifications>} yields Object[]
1816
1817Notifications are regular objects.
1818
1819*/
1820
1821CodeGradX.Campaign.prototype.getNotifications = function (count, from) {
1822 let state = CodeGradX.getCurrentState();
1823 let campaign = this;
1824 state.debug('getNotifications1', from, count);
1825 function processResponse (response) {
1826 state.debug('getNotifications2', response);
1827 return response.entity;
1828 }
1829 let headers = {
1830 "Accept": 'application/json',
1831 'Content-Type': 'application/x-www-form-urlencoded'
1832 };
1833 count = count ||
1834 CodeGradX.Campaign.prototype.getNotifications.default.count;
1835 let entity = { count };
1836 if ( from ) {
1837 entity.from = from;
1838 }
1839 return state.sendAXServer('x', {
1840 path: ('/notification/campaign/' + campaign.name),
1841 method: 'POST',
1842 headers: headers,
1843 entity: entity
1844 }).then(processResponse);
1845};
1846CodeGradX.Campaign.prototype.getNotifications.default = {
1847 count: 10
1848};
1849
1850/** get the best job reports
1851
1852 @param {Exercise} exercise - an Exercise of the campaign
1853 @returns {Promise<Jobs>} yields Job[]
1854
1855 {"jobs":[
1856 {"person_id":2237361,
1857 "totalMark":1,
1858 "archived":"2017-07-08T20:04:49",
1859 "uuid":"B28BE79C641811E7901800E6ED542A8E",
1860 "mark":0,
1861 "exercise_nickname":"notes",
1862 "kind":"job",
1863 "exercise_uuid":"11111111111199970003201704080001",
1864 "exercise_name":"cnam.mooc.socle.notes.1",
1865 "started":"2017-07-08T20:04:52",
1866 "finished":"2017-07-08T20:04:52"},
1867 ... ]}
1868
1869*/
1870
1871CodeGradX.Campaign.prototype.getTopJobs = function (exercise) {
1872 let state = CodeGradX.getCurrentState();
1873 let campaign = this;
1874 state.debug('getTopJobs1', exercise);
1875 function processResponse (response) {
1876 state.debug('getTopJobs2', response);
1877 let jobs = response.entity.jobs;
1878 return jobs.map(CodeGradX.Job.js2job);
1879 }
1880 let headers = {
1881 "Accept": 'application/json',
1882 'Content-Type': 'application/x-www-form-urlencoded'
1883 };
1884 let exoUUID = exercise.uuid.replace(/-/, '');
1885 return state.sendAXServer('x', {
1886 path: ('/exercise/campaign/' + campaign.name + '/' + exoUUID),
1887 method: 'GET',
1888 headers: headers
1889 }).then(processResponse);
1890};
1891
1892// **************** Exercise ***************************
1893
1894/** Exercise. When extracted from a Campaign, an Exercise looks like:
1895
1896 { name: 'org.fw4ex.li101.croissante.0',
1897 nickname: 'croissante',
1898 safecookie: 'UhSn..3nyUSQWNtqwm_c6w@@',
1899 summary: 'Déterminer si une liste est croissante',
1900 tags: [ 'li101', 'scheme', 'fonction' ] }
1901
1902 This information is sufficient to list the exercises with a short
1903 description of their stem. If you need more information (the stem
1904 for instance), use the `getDescription` method.
1905
1906 @constructor
1907 @property {string} name - full name
1908 @property {string} nickname - short name
1909 @property {string} safecookie - long crypted identifier
1910 @property {string} summary - single sentence qualifying the Exercise
1911 @property {Array<string>} tags - Array of tags categorizing the Exercise.
1912 @property {string} server - base URL of the server that served the exercise
1913
1914 The `getDescription()` method completes the description of an Exercise
1915 with the following fields:
1916
1917 @property {XMLstring} _XMLdescription - raw XML description
1918 @property {Object} _description - description
1919 @property {Array<Author>} authorship - Array of authorship
1920 @property {XMLstring} XMLstem - raw XML stem
1921 @property {string} stem - default HTML translation of the XML stem
1922 @property {Object} expectations - files expected in student's answer
1923 @property {Object} equipment - files to be given to the student
1924
1925 This field may be present if there is a single file in expectations:
1926
1927 @property {string} inlineFileName - single file expected in student's answer
1928
1929 */
1930
1931CodeGradX.Exercise = function (js) {
1932 function normalizeUUID (uuid) {
1933 const uuidRegexp = /^(.{8})(.{4})(.{4})(.{4})(.{12})$/;
1934 return uuid.replace(/-/g, '').replace(uuidRegexp, "$1-$2-$3-$4-$5");
1935 }
1936 if ( js.uuid && ! js.exerciseid ) {
1937 js.exerciseid = normalizeUUID(js.uuid);
1938 }
1939 if ( js.uuid && ! js.location ) {
1940 js.location = '/e' + js.uuid.replace(/-/g, '').replace(/(.)/g, "/$1");
1941 }
1942 Object.assign(this, js);
1943};
1944
1945CodeGradX.Exercise.js2exercise = function (js) {
1946 return new CodeGradX.Exercise(js);
1947};
1948
1949/** Get the XML descriptor of the Exercise.
1950 This XML descriptor will enrich the Exercise instance.
1951 The raw XML string is stored under property 'XMLdescription', the
1952 decoded XML string is stored under property 'description'.
1953
1954 Caution: this description is converted from XML to a Javascript
1955 object with xml2js idiosyncrasies.
1956
1957 @returns {Promise<ExerciseDescription>} yields {ExerciseDescription}
1958
1959 */
1960
1961CodeGradX.Exercise.prototype.getDescription = function () {
1962 const exercise = this;
1963 const state = CodeGradX.getCurrentState();
1964 state.debug('getDescription1', exercise);
1965 if ( exercise._description ) {
1966 return when(exercise._description);
1967 }
1968 if ( ! exercise.safecookie ) {
1969 return when.reject("Non deployed exercise " + exercise.name);
1970 }
1971 const promise = state.sendESServer('e', {
1972 path: ('/exercisecontent/' + exercise.safecookie + '/content'),
1973 method: 'GET',
1974 headers: {
1975 Accept: "text/xml",
1976 // useful for debug:
1977 "X-CodeGradX-Comment": `ExerciseName=${exercise.name}`
1978 }
1979 });
1980 // Parse the HTTP response, translate the XML into a Javascript object
1981 // and provide it to the sequel:
1982 const promise1 = promise.then(function (response) {
1983 state.debug('getDescription2', response);
1984 //console.log(response);
1985 exercise.server = response.url.replace(
1986 new RegExp('^(https?://[^/]+)/.*$'), "$1");
1987 exercise._XMLdescription = response.entity;
1988 function parseXML (description) {
1989 state.debug('getDescription2b', description);
1990 exercise._description = description;
1991 //description._exercise = exercise;
1992 return when(description);
1993 }
1994 return CodeGradX.parsexml(exercise._XMLdescription).then(parseXML);
1995 });
1996 const promise3 = promise.then(function (response) {
1997 // Extract stem
1998 state.debug("getDescription4", response);
1999 const contentRegExp = new RegExp("^(.|\n)*(<\s*content\s*>(.|\n)*</content\s*>)(.|\n)*$");
2000 const content = response.entity.replace(contentRegExp, "$2");
2001 exercise.XMLcontent = content;
2002 exercise.stem = CodeGradX.xml2html(content, { exercise });
2003 // extract equipment:
2004 state.debug("getDescription5b", exercise);
2005 extractEquipment(exercise, response.entity);
2006 // extract identity and authorship:
2007 state.debug("getDescription6", exercise);
2008 return extractIdentification(exercise, response.entity);
2009 });
2010 const promise4 = promise.then(function (response) {
2011 // If only one question expecting only one file, retrieve its name:
2012 state.debug('getDescription5c');
2013 const expectationsRegExp =
2014 new RegExp("<\s*expectations\s*>((.|\n)*?)</expectations\s*>", "g");
2015 function concat (s1, s2) {
2016 return s1 + s2;
2017 }
2018 const expectationss = response.entity.match(expectationsRegExp);
2019 if ( expectationss ) {
2020 const files = _.reduce(expectationss, concat);
2021 const expectations = '<div>' + files + '</div>';
2022 return CodeGradX.parsexml(expectations).then(function (result) {
2023 state.debug('getDescription5a');
2024 if ( Array.isArray(result.div.expectations.file) ) {
2025 // to be done. Maybe ? Why ?
2026 } else {
2027 //console.log(result.div.expectations);
2028 exercise.expectations = result.div.expectations;
2029 exercise.inlineFileName = result.div.expectations.file.$.basename;
2030 }
2031 return when(response);
2032 }).catch(function (/*reason*/) {
2033 exercise.expectations = [];
2034 return when(response);
2035 });
2036 } else {
2037 exercise.expectations = [];
2038 return when(response);
2039 }
2040 });
2041 return when.join(promise3, promise4)
2042 .then(function (/*values*/) {
2043 return promise1;
2044 });
2045};
2046
2047/** Get an equipment file that is a file needed by the students
2048 and stored in the exercise.
2049
2050 @param {string} file - the name of the file
2051 @returns {Promise<>}
2052
2053*/
2054
2055CodeGradX.Exercise.prototype.getEquipmentFile = function (file) {
2056 const exercise = this;
2057 const state = CodeGradX.getCurrentState();
2058 state.debug('getEquipmentFile1', exercise, file);
2059 if ( ! exercise.safecookie ) {
2060 return when.reject("Non deployed exercise " + exercise.name);
2061 }
2062 const promise = state.sendESServer('e', {
2063 path: ('/exercisecontent/' + exercise.safecookie + '/path' + file),
2064 method: 'GET',
2065 headers: {
2066 Accept: "*/*"
2067 }
2068 });
2069 return promise.catch(function (reason) {
2070 console.log(reason);
2071 return when.reject(reason);
2072 });
2073};
2074
2075/** Convert an XML fragment describing the identification of an
2076 exercise and stuff the Exercise instance.
2077
2078 <identification name="" date="" nickname="">
2079 <summary></summary>
2080 <tags></tags>
2081 <authorship></authorship>
2082 </identification>
2083
2084*/
2085
2086const identificationRegExp =
2087 new RegExp("^(.|\n)*(<\s*identification +(.|\n)*</identification\s*>)(.|\n)*$");
2088const summaryRegExp =
2089 new RegExp("^(.|\n)*(<\s*summary.*?>(.|\n)*</summary\s*>)(.|\n)*$");
2090
2091function extractIdentification (exercise, s) {
2092 const content = s.replace(identificationRegExp, "$2");
2093 return CodeGradX.parsexml(content).then(function (result) {
2094 if ( ! result.identification ) {
2095 return when(exercise);
2096 }
2097 result = result.identification;
2098 // extract identification:
2099 exercise.name = result.$.name;
2100 exercise.nickname = result.$.nickname;
2101 exercise.date = result.$.date;
2102 const summary = content.replace(summaryRegExp, "$2");
2103 exercise.summary = CodeGradX.xml2html(summary);
2104 if ( Array.isArray(result.tags.tag) ) {
2105 exercise.tags = result.tags.tag.map(function (tag) {
2106 return tag.$.name;
2107 });
2108 } else {
2109 exercise.tags = [result.tags.tag.$.name];
2110 }
2111 // extract authors
2112 const authors = result.authorship;
2113 if ( Array.isArray(authors.author) ) {
2114 exercise.authorship = authors.author;
2115 } else {
2116 exercise.authorship = [ authors.author ];
2117 }
2118 return when(exercise);
2119 });
2120}
2121
2122/** Convert an XML fragment describing directories and files into
2123 pathnames. For instance,
2124
2125 <expectations>
2126 <file basename='foo'/>
2127 <directory basename='bar'>
2128 <file basename='hux'/>
2129 <file basename='wek'/>
2130 </directory>
2131 </expectations>
2132
2133 will be converted into
2134
2135 [ '/foo', '/bar/hux', '/bar/wek']
2136
2137
2138function extractExpectations (exercice, s) {
2139 return exercise;
2140}
2141
2142*/
2143
2144/** Convert an XML fragment describing directories and files into
2145 pathnames. For instance,
2146
2147 <equipment>
2148 <file basename='foo'/>
2149 <directory basename='bar'>
2150 <file basename='hux'/>
2151 <file basename='wek'/>
2152 </directory>
2153 </equipment>
2154
2155 will be converted into
2156
2157 [ '/foo', '/bar/hux', '/bar/wek']
2158
2159*/
2160
2161function extractEquipment (exercise, s) {
2162 exercise.equipment = [];
2163 const equipmentRegExp = new RegExp(
2164 "^(.|\n)*(<equipment>\s*(.|\n)*?\s*</equipment>)(.|\n)*$");
2165 const content = s.replace(equipmentRegExp, "$2");
2166 if ( s.length === content.length ) {
2167 // No equipment!
2168 return exercise;
2169 }
2170 function flatten (o, dir) {
2171 let results = [];
2172 if ( o.directory ) {
2173 if ( Array.isArray(o.directory) ) {
2174 o.directory.forEach(function (o) {
2175 const newdir = dir + '/' + o.$.basename;
2176 results = results.concat(flatten(o, newdir));
2177 });
2178 } else {
2179 const newdir = dir + '/' + o.directory.$.basename;
2180 results = results.concat(flatten(o.directory, newdir));
2181 }
2182 }
2183 if ( o.file ) {
2184 if ( Array.isArray(o.file) ) {
2185 o.file.forEach(function (o) {
2186 results = results.concat(flatten(o, dir));
2187 });
2188 } else {
2189 o = o.file;
2190 }
2191 }
2192 if ( !o.file && !o.directory && o.$ && o.$.basename && ! o.$.hidden ) {
2193 results.push(dir + '/' + o.$.basename);
2194 }
2195 return results;
2196 }
2197 if ( content.length > 0 ) {
2198 try {
2199 const parser = new xml2js.Parser({
2200 explicitArray: false,
2201 trim: true
2202 });
2203 parser.parseString(content, function (err, result) {
2204 exercise.equipment = flatten(result.equipment, '');
2205 });
2206 } catch (e) {
2207 const state = CodeGradX.getCurrentState();
2208 state.debug("extractEquipment", e);
2209 }
2210 }
2211 return exercise;
2212}
2213
2214/** Promisify an XML to Javascript converter.
2215
2216 @param {string} xml - string to parse
2217 @returns {Promise}
2218
2219 */
2220
2221CodeGradX.parsexml = function (xml) {
2222 if ( ! xml ) {
2223 return when.reject("Cannot parse XML " + xml);
2224 }
2225 const parser = new xml2js.Parser({
2226 explicitArray: false,
2227 trim: true
2228 });
2229 let xerr, xresult;
2230 try {
2231 parser.parseString(xml, function (err, result) {
2232 xerr = err;
2233 xresult = result;
2234 });
2235 } catch (e) {
2236 // for a TypeError: Cannot read property 'toString' of undefined
2237 return when.reject(e);
2238 }
2239 if ( xerr ) {
2240 return when.reject(xerr);
2241 } else {
2242 return when(xresult);
2243 }
2244};
2245
2246/** Send a string as the proposed solution to an Exercise.
2247 Returns a Job on which you may invoke the `getReport` method.
2248
2249 @param {string} answer
2250 @returns {Promise<Job>} yields {Job}
2251
2252 */
2253
2254CodeGradX.Exercise.prototype.sendStringAnswer = function (answer) {
2255 const exercise = this;
2256 const state = CodeGradX.getCurrentState();
2257 state.debug('sendStringAnswer1', answer);
2258 if ( ! exercise.safecookie ) {
2259 return when.reject("Non deployed exercise " + exercise.name);
2260 }
2261 if ( typeof exercise.inlineFileName === 'undefined') {
2262 if ( exercise._description ) {
2263 return when.reject(new Error("Non suitable exercise"));
2264 } else {
2265 return exercise.getDescription()
2266 .then(function (/*description*/) {
2267 return exercise.sendStringAnswer(answer);
2268 });
2269 }
2270 }
2271 function processResponse (response) {
2272 //console.log(response);
2273 state.debug('sendStringAnswer2', response);
2274 return CodeGradX.parsexml(response.entity).then(function (js) {
2275 //console.log(js);
2276 state.debug('sendStringAnswer3', js);
2277 js = js.fw4ex.jobSubmittedReport;
2278 exercise.uuid = js.exercise.$.exerciseid;
2279 const job = new CodeGradX.Job({
2280 exercise: exercise,
2281 content: answer,
2282 responseXML: response.entity,
2283 response: js,
2284 personid: CodeGradX._str2num(js.person.$.personid),
2285 archived: CodeGradX._str2Date(js.job.$.archived),
2286 jobid: js.job.$.jobid,
2287 pathdir: js.$.location
2288 });
2289 return when(job);
2290 });
2291 }
2292 const content = new Buffer(answer, 'utf8');
2293 const headers = {
2294 "Content-Type": "application/octet-stream",
2295 "Content-Disposition": ("inline; filename=" + exercise.inlineFileName),
2296 "Accept": 'text/xml'
2297 };
2298 if ( CodeGradX.isNode() ) {
2299 headers["Content-Length"] = content.length;
2300 }
2301 return state.sendAXServer('a', {
2302 path: ('/exercise/' + exercise.safecookie + '/job'),
2303 method: "POST",
2304 headers: headers,
2305 entity: content
2306 }).then(processResponse);
2307};
2308
2309/** Send the content of a file selected by an input:file widget in the
2310 * browser. Returns a Job on which you may invoke the `getReport` method.
2311
2312 @param {DOM} form DOM element
2313 @returns {Promise<Job>} yields {Job}
2314
2315The form DOM element must contain an <input type='file' name='content'>
2316element. This code only runs in a browser providing the FormData class.
2317
2318*/
2319
2320CodeGradX.Exercise.prototype.sendFileFromDOM = function (form) {
2321 const exercise = this;
2322 const state = CodeGradX.getCurrentState();
2323 state.debug('sendZipFileAnswer1', FW4EX.currentFileName);
2324 if ( ! exercise.safecookie ) {
2325 return when.reject("Non deployed exercise " + exercise.name);
2326 }
2327 function processResponse (response) {
2328 //console.log(response);
2329 state.debug('sendZipFileAnswer2', response);
2330 return CodeGradX.parsexml(response.entity).then(function (js) {
2331 //console.log(js);
2332 state.debug('sendZipFileAnswer3', js);
2333 js = js.fw4ex.jobSubmittedReport;
2334 exercise.uuid = js.exercise.$.exerciseid;
2335 const job = new CodeGradX.Job({
2336 exercise: exercise,
2337 content: FW4EX.currentFileName,
2338 responseXML: response.entity,
2339 response: js,
2340 personid: CodeGradX._str2num(js.person.$.personid),
2341 archived: CodeGradX._str2Date(js.job.$.archived),
2342 jobid: js.job.$.jobid,
2343 pathdir: js.$.location
2344 });
2345 return when(job);
2346 });
2347 }
2348 const basefilename = FW4EX.currentFileName.replace(new RegExp("^.*/"), '');
2349 const headers = {
2350 "Content-Type": "multipart/form-data",
2351 "Content-Disposition": ("inline; filename=" + basefilename),
2352 "Accept": 'text/xml'
2353 };
2354 const fd = new FormData(form);
2355 return state.sendAXServer('a', {
2356 path: ('/exercise/' + exercise.safecookie + '/job'),
2357 method: "POST",
2358 headers: headers,
2359 entity: fd
2360 }).then(processResponse);
2361};
2362
2363/** Send a batch of files that is, multiple answers to be marked
2364 against an Exercise. That file is selected with an input:file
2365 widget in the browser.
2366
2367 @param {DOMform} form - the input:file widget
2368 @returns {Promise<Batch>} yielding a Batch.
2369
2370The form DOM element must contain an <input type='file' name='content'>
2371element. This code only runs in a browser providing the FormData class.
2372
2373*/
2374
2375CodeGradX.Exercise.prototype.sendBatchFromDOM = function (form) {
2376 const exercise = this;
2377 const state = CodeGradX.getCurrentState();
2378 state.debug('sendBatchFile1');
2379 if ( ! exercise.safecookie ) {
2380 return when.reject("Non deployed exercise " + exercise.name);
2381 }
2382 function processResponse (response) {
2383 //console.log(response);
2384 state.debug('sendBatchFile2', response);
2385 return CodeGradX.parsexml(response.entity).then(function (js) {
2386 //console.log(js);
2387 state.debug('sendBatchFile3', js);
2388 js = js.fw4ex.multiJobSubmittedReport;
2389 exercise.uuid = js.exercise.$.exerciseid;
2390 const batch = new CodeGradX.Batch({
2391 exercise: exercise,
2392 responseXML: response.entity,
2393 response: js,
2394 personid: CodeGradX._str2num(js.person.$.personid),
2395 archived: CodeGradX._str2Date(js.batch.$.archived),
2396 batchid: js.batch.$.batchid,
2397 pathdir: js.$.location,
2398 finishedjobs: 0
2399 });
2400 return when(batch);
2401 });
2402 }
2403 const basefilename = FW4EX.currentFileName.replace(new RegExp("^.*/"), '');
2404 const headers = {
2405 "Content-Type": "multipart/form-data",
2406 "Content-Disposition": ("inline; filename=" + basefilename),
2407 "Accept": 'text/xml'
2408 };
2409 const fd = new FormData(form);
2410 return state.sendAXServer('a', {
2411 path: ('/exercise/' + exercise.safecookie + '/batch'),
2412 method: "POST",
2413 headers: headers,
2414 entity: fd
2415 }).then(processResponse);
2416};
2417
2418/** After submitting a new Exercise, get Exercise autocheck reports
2419 that is, the job reports corresponding to the pseudo-jobs
2420 contained in the Exercise TGZ file.
2421
2422 @param {Object} parameters - @see CodeGradX.sendRepeatedlyESServer
2423 @returns {Promise<Exercise>} yielding an Exercise
2424
2425 The `getExerciseReport()` method will add some new fields to the
2426 Exercise object:
2427
2428 @property {XMLstring} XMLauthorReport - raw XML report
2429 @property {string} globalReport - global report
2430 @property {number} totaljobs - the total number of pseudo-jobs
2431 @property {number} finishedjobs - the number of marked pseudo-jobs
2432 @property {Hashtable<Job>} pseudojobs - Hashtable of pseudo-jobs
2433
2434 For each pseudo-job, are recorded all the fields of a regular Job
2435 plus some additional fields such as `duration`.
2436
2437 The globalReport is the report independent of the pseudojob reports.
2438
2439 If the exercise is successfully autochecked, it may be used by
2440 `sendStringAnswer()`, `sendFileAnswer()` or `sendBatch()` methods
2441 using the additional `safecookie` field:
2442
2443 @property {string} safecookie - the long identifier of the exercise.
2444
2445A failure might be:
2446
2447 <fw4ex version="1.0">
2448 <exerciseAuthorReport exerciseid="9A9701A8-CE17-11E7-AB9A-DBAB25888DB0">
2449 <report>
2450 </report>
2451 </exerciseAuthorReport>
2452 </fw4ex>
2453
2454*/
2455
2456CodeGradX.Exercise.prototype.getExerciseReport = function (parameters) {
2457 const exercise = this;
2458 const state = CodeGradX.getCurrentState();
2459 state.debug("getExerciseReport1", exercise, parameters);
2460 if ( exercise.finishedjobs ) {
2461 return when(exercise);
2462 }
2463 function processResponse (response) {
2464 state.debug("getExerciseReport2", response);
2465 //console.log(response);
2466 exercise.originServer = response.url.replace(/^(.*)\/s\/.*$/, "$1");
2467 exercise.XMLauthorReport = response.entity;
2468 function catchXMLerror (reason) {
2469 state.debug("catchXMLerror", reason);
2470 return when.reject(reason);
2471 }
2472 state.debug("getExerciseReport3a");
2473 return extractIdentification(exercise, response.entity)
2474 .then(function (/*exercise*/) {
2475 state.debug("getExerciseReport3b");
2476 return CodeGradX.parsexml(response.entity);
2477 }).then(function (js) {
2478 state.debug("getExerciseReport3c", js);
2479 js = js.fw4ex.exerciseAuthorReport;
2480 exercise.pseudojobs = {};
2481 exercise._pseudojobs = [];
2482 if ( js.report ) {
2483 exercise.globalReport = js.report;
2484 if ( ! js.pseudojobs || js.pseudojobs.length === 0 ) {
2485 return when(exercise);
2486 }
2487 }
2488 exercise.totaljobs =
2489 CodeGradX._str2num(js.pseudojobs.$.totaljobs);
2490 exercise.finishedjobs =
2491 CodeGradX._str2num(js.pseudojobs.$.finishedjobs);
2492 function processPseudoJob (jspj) {
2493 const name = jspj.submission.$.name;
2494 const job = new CodeGradX.Job({
2495 exercise: exercise,
2496 XMLpseudojob: jspj,
2497 jobid: jspj.$.jobid,
2498 pathdir: jspj.$.location,
2499 duration: CodeGradX._str2num(jspj.$.duration),
2500 problem: false,
2501 label: name
2502 // partial marks TOBEDONE
2503 });
2504 if ( jspj.marking ) {
2505 job.expectedMark = CodeGradX._str2num2decimals(
2506 jspj.submission.$.expectedMark);
2507 job.mark = CodeGradX._str2num2decimals(
2508 jspj.marking.$.mark);
2509 job.totalMark = CodeGradX._str2num2decimals(
2510 jspj.marking.$.totalMark);
2511 job.archived =
2512 CodeGradX._str2Date(jspj.marking.$.archived);
2513 job.started =
2514 CodeGradX._str2Date(jspj.marking.$.started);
2515 job.ended =
2516 CodeGradX._str2Date(jspj.marking.$.ended);
2517 job.finished =
2518 CodeGradX._str2Date(jspj.marking.$.finished);
2519 }
2520 if ( jspj.$.problem ) {
2521 job.problem = true;
2522 if ( jspj.report ) {
2523 job.problem = jspj.report;
2524 }
2525 }
2526 exercise.pseudojobs[name] = job;
2527 exercise._pseudojobs.push(job);
2528 }
2529 const pseudojobs = js.pseudojobs.pseudojob;
2530 if ( Array.isArray(pseudojobs) ) {
2531 pseudojobs.forEach(processPseudoJob);
2532 } else if ( pseudojobs ) {
2533 processPseudoJob(pseudojobs);
2534 } else {
2535 // nothing! exercise.finishedjobs is probably 0!
2536 }
2537 //console.log(exercise); // DEBUG
2538 if ( js.$.safecookie ) {
2539 exercise.safecookie = js.$.safecookie;
2540 }
2541 return when(exercise);
2542 })
2543 .catch(catchXMLerror);
2544 }
2545 return state.sendRepeatedlyESServer('s', parameters, {
2546 path: exercise.getExerciseReportURL(),
2547 method: 'GET',
2548 headers: {
2549 "Accept": 'text/xml'
2550 }
2551 }).then(processResponse);
2552};
2553
2554CodeGradX.Exercise.prototype.getBaseURL = function () {
2555 const exercise = this;
2556 const path = exercise.location + '/' + exercise.exerciseid;
2557 return path;
2558};
2559CodeGradX.Exercise.prototype.getExerciseReportURL = function () {
2560 const exercise = this;
2561 return exercise.getBaseURL() + '.xml';
2562};
2563CodeGradX.Exercise.prototype.getTgzURL = function () {
2564 const exercise = this;
2565 return exercise.getBaseURL() + '.tgz';
2566};
2567
2568// **************** ExercisesSet ***************************
2569
2570/** Initialize a set (in fact a tree) of Exercises with some json such as:
2571
2572 { "notice": ?,
2573 "content": [
2574 { "title": "",
2575 "exercises": [
2576 { "name": "", ...}, ...
2577 ]
2578 },
2579 ...
2580 ]}
2581
2582 The tree is made of nodes. Each node may contain some properties
2583 such as `title`, `prologue` (sentences introducing a set of exercises),
2584 `epilogue` (sentences ending a set of exercises) and `exercises` an
2585 array of Exercises or ExercisesSet.
2586
2587 @constructor
2588 @property {string} title
2589 @property {string} prologue
2590 @property {string} epilogue
2591 @property {Array} exercises - Array of Exercises or ExercisesSet.
2592
2593 */
2594
2595CodeGradX.ExercisesSet = function (json) {
2596 if ( json.content ) {
2597 // skip 'notice', get array of sets of exercises:
2598 json = json.content;
2599 }
2600 // Here: json is an array of exercises or sets of exercises:
2601 function processItem (json) {
2602 if ( json.exercises ) {
2603 return new CodeGradX.ExercisesSet(json);
2604 } else {
2605 if ( json.name && json.nickname ) {
2606 return new CodeGradX.Exercise(json);
2607 } else {
2608 throw new Error("Not an exercise " + JSON.stringify(json));
2609 }
2610 }
2611 }
2612 if ( Array.isArray(json) ) {
2613 // no title, prologue nor epilogue.
2614 this.exercises = _.map(json, processItem);
2615 } else {
2616 // initialize optional title, prologue, epilogue:
2617 Object.assign(this, json);
2618 this.exercises = _.map(json.exercises, processItem);
2619 }
2620};
2621
2622/** Fetch a precise ExercisesSet. This is mainly used to update the
2623 current set of exercises. Attention, this is not a method but a
2624 static function!
2625
2626 @param {String} path - URI of the exercises.json file
2627 @returns {Promise<ExercisesSet>} yields ExercisesSet
2628
2629*/
2630
2631CodeGradX.ExercisesSet.getExercisesSet = function (name) {
2632 const state = CodeGradX.getCurrentState();
2633 state.debug('UgetExercisesSet1', name);
2634 const p3 = state.sendAXServer('x', {
2635 path: ('/exercisesset/path/' + name),
2636 method: 'GET',
2637 headers: {
2638 Accept: "application/json"
2639 }
2640 });
2641 return p3.then(function (response) {
2642 state.debug('UgetExercisesSet2', response);
2643 const exercisesSet = new CodeGradX.ExercisesSet(response.entity);
2644 return when(exercisesSet);
2645 }).catch(function (reason) {
2646 state.debug('UgetExercisesSet3', reason);
2647 return when(undefined);
2648 });
2649};
2650
2651/** Find an exercise by its name in an ExercisesSet that is,
2652 a tree of Exercises.
2653
2654 @param {String|Number} name
2655 @returns {Exercise}
2656
2657 */
2658
2659CodeGradX.ExercisesSet.prototype.getExercise = function (name) {
2660 const exercises = this;
2661 if ( _.isNumber(name) ) {
2662 return exercises.getExerciseByIndex(name);
2663 } else {
2664 return exercises.getExerciseByName(name);
2665 }
2666};
2667
2668CodeGradX.ExercisesSet.prototype.getExerciseByName = function (name) {
2669 const exercisesSet = this;
2670 //console.log(exercisesSet);// DEBUG
2671 function find (thing) {
2672 if ( thing instanceof CodeGradX.ExercisesSet ) {
2673 const exercises = thing.exercises;
2674 for ( let i=0 ; i<exercises.length ; i++ ) {
2675 //console.log("explore " + i + '/' + exercises.length);
2676 const result = find(exercises[i]);
2677 if ( result ) {
2678 return result;
2679 }
2680 }
2681 return false;
2682 } else if ( thing instanceof CodeGradX.Exercise ) {
2683 const exercise = thing;
2684 //console.log("compare with " + exercise.name);
2685 if ( exercise.name === name ) {
2686 return exercise;
2687 } else {
2688 return false;
2689 }
2690 } else {
2691 throw new Error("Not an Exercise nor an ExerciseSet", thing);
2692 }
2693 }
2694 return find(exercisesSet);
2695};
2696
2697CodeGradX.ExercisesSet.prototype.getExerciseByIndex = function (index) {
2698 const exercises = this;
2699 function find (exercises) {
2700 if ( Array.isArray(exercises) ) {
2701 for ( let i=0 ; i<exercises.length ; i++ ) {
2702 //console.log("explore " + i); // DEBUG
2703 const result = find(exercises[i]);
2704 if ( result ) {
2705 return result;
2706 }
2707 }
2708 return false;
2709 } else if ( exercises instanceof CodeGradX.ExercisesSet ) {
2710 return find(exercises.exercises);
2711 } else if ( exercises instanceof CodeGradX.Exercise ) {
2712 if ( index === 0 ) {
2713 return exercises;
2714 } else {
2715 //console.log('index= ' + index); // DEBUG
2716 index--;
2717 return false;
2718 }
2719 }
2720 }
2721 return find(exercises);
2722};
2723
2724// **************** Job ***************************
2725
2726/** A Job corresponds to an attempt of solving an Exercise.
2727 A Job is obtained with `sendStringAnswer` or `sendFileAnswer`.
2728 From a job, you may get the marking report with `getReport`.
2729
2730 @constructor
2731 @property {string} XMLreport - raw XML report
2732 @property {string} HTMLreport - default HTML from XML report
2733
2734*/
2735
2736CodeGradX.Job = function (js) {
2737 function normalizeUUID (uuid) {
2738 const uuidRegexp = /^(.{8})(.{4})(.{4})(.{4})(.{12})$/;
2739 return uuid.replace(/-/g, '').replace(uuidRegexp, "$1-$2-$3-$4-$5");
2740 }
2741 if ( js.uuid && ! js.jobid ) {
2742 js.jobid = normalizeUUID(js.uuid);
2743 }
2744 js.mark = CodeGradX._str2num2decimals(js.mark);
2745 js.totalMark = CodeGradX._str2num2decimals(js.totalMark);
2746 if ( js.jobid && ! js.pathdir ) {
2747 js.pathdir = '/s' + js.jobid.replace(/-/g, '').replace(/(.)/g, "/$1");
2748 }
2749 Object.assign(this, js);
2750};
2751
2752CodeGradX.Job.js2job = function (js) {
2753 return new CodeGradX.Job(js);
2754};
2755
2756/** Get the marking report of that Job. The marking report will be stored
2757 in the `XMLreport` and `report` properties.
2758
2759 @param {Object} parameters - for repetition see sendRepeatedlyESServer.default
2760 @returns {Promise} yields {Job}
2761
2762 */
2763
2764CodeGradX.Job.prototype.getReport = function (parameters) {
2765 parameters = parameters || {};
2766 const job = this;
2767 const state = CodeGradX.getCurrentState();
2768 state.debug('getJobReport1', job);
2769 if ( job.XMLreport ) {
2770 return when(job);
2771 }
2772 const path = job.getReportURL();
2773 const promise = state.sendRepeatedlyESServer('s', parameters, {
2774 path: path,
2775 method: 'GET',
2776 headers: {
2777 "Accept": "text/xml"
2778 }
2779 });
2780 const promise1 = promise.then(function (response) {
2781 //state.log.show();
2782 //console.log(response);
2783 state.debug('getJobReport2', job);
2784 job.originServer = response.url.replace(/^(.*)\/s\/.*$/, "$1");
2785 job.XMLreport = response.entity;
2786 return when(job);
2787 }).catch(function (reasons) {
2788 // sort reasons and extract only waitedTooMuch if present:
2789 function tooLongWaiting (reasons) {
2790 if ( Array.isArray(reasons) ) {
2791 for ( let i = 0 ; i<reasons.length ; i++ ) {
2792 const r = reasons[i];
2793 const result = tooLongWaiting(r);
2794 if ( result ) {
2795 return result;
2796 }
2797 }
2798 } else if ( reasons instanceof Error ) {
2799 if ( reasons.message.match(/waitedTooMuch/) ) {
2800 return reasons;
2801 }
2802 }
2803 return undefined;
2804 }
2805 const result = tooLongWaiting(reasons);
2806 return when.reject(result || reasons);
2807 });
2808 const promise2 = promise.then(function (response) {
2809 // Fill archived, started, ended, finished, mark and totalMark
2810 state.debug('getJobReport3', job);
2811 const markingRegExp = new RegExp("^(.|\n)*(<marking (.|\n)*?>)(.|\n)*$");
2812 let marking = response.entity.replace(markingRegExp, "$2");
2813 state.debug('getJobReport3 marking', marking);
2814 //console.log(marking); //DEBUG
2815 if ( marking.length === response.entity.length ) {
2816 return when.reject(response);
2817 }
2818 marking = marking.replace(/>/, "/>");
2819 //console.log(marking);
2820 return CodeGradX.parsexml(marking).then(function (js) {
2821 job.mark = CodeGradX._str2num2decimals(js.marking.$.mark);
2822 job.totalMark = CodeGradX._str2num2decimals(js.marking.$.totalMark);
2823 job.archived = CodeGradX._str2Date(js.marking.$.archived);
2824 job.started = CodeGradX._str2Date(js.marking.$.started);
2825 job.ended = CodeGradX._str2Date(js.marking.$.ended);
2826 job.finished = CodeGradX._str2Date(js.marking.$.finished);
2827 // machine, partial marks TO BE DONE
2828 return when(response);
2829 });
2830 });
2831 const promise3 = promise.then(function (response) {
2832 // Fill exerciseid (already in exercise.uuid !)
2833 state.debug('getJobReport4', job);
2834 const exerciseRegExp = new RegExp("^(.|\n)*(<exercise (.|\n)*?>)(.|\n)*$");
2835 const exercise = response.entity.replace(exerciseRegExp, "$2");
2836 if ( exercise.length === response.entity.length ) {
2837 return when.reject(response);
2838 }
2839 //console.log(exercise);
2840 return CodeGradX.parsexml(exercise).then(function (js) {
2841 Object.assign(job, js.exercise.$);
2842 return when(response);
2843 });
2844 });
2845 const promise4 = promise.then(function (response) {
2846 // Fill report
2847 state.debug('getJobReport5');
2848 const contentRegExp = new RegExp("^(.|\n)*(<report>(.|\n)*?</report>)(.|\n)*$");
2849 const content = response.entity.replace(contentRegExp, "$2");
2850 //state.debug('getJobReport5 content',
2851 // content.length, response.entity.length);
2852 if ( content.length === response.entity.length ) {
2853 return when.reject(response);
2854 }
2855 job.HTMLreport = CodeGradX.xml2html(content);
2856 return when(response);
2857 });
2858 return when.join(promise2, promise3, promise4).then(function (/*values*/) {
2859 state.debug('getJobReport6', job);
2860 //console.log(job);
2861 return promise1;
2862 }).finally(function () {
2863 return promise1;
2864 });
2865};
2866
2867/** Get the problem report of that Job if it exists. The marking
2868 report will be stored in the `XMLproblemReport` property. If no
2869 problem report exists, the returned promise is rejected.
2870
2871 @param {Object} parameters - for repetition see sendRepeatedlyESServer.default
2872 @returns {Promise} yields {Job}
2873
2874 */
2875
2876CodeGradX.Job.prototype.getProblemReport = function (parameters) {
2877 parameters = parameters || {};
2878 const job = this;
2879 const state = CodeGradX.getCurrentState();
2880 state.debug('getJobProblemReport1', job);
2881 if ( ! job.problem ) {
2882 return when.reject("No problem report");
2883 }
2884 if ( job.XMLproblemReport ) {
2885 return when(job);
2886 }
2887 const path = job.getProblemReportURL();
2888 const promise = state.sendRepeatedlyESServer('s', parameters, {
2889 path: path,
2890 method: 'GET',
2891 headers: {
2892 "Accept": "text/xml"
2893 }
2894 });
2895 const promise1 = promise.then(function (response) {
2896 //state.log.show();
2897 //console.log(response);
2898 state.debug('getJobProblemReport2', job);
2899 job.originServer = response.url.replace(/^(.*)\/s\/.*$/, "$1");
2900 job.XMLproblemReport = response.entity;
2901 return when(job);
2902 });
2903 return promise1;
2904};
2905
2906/** Compute the URL that form the base URL to access directly the
2907 report, the problem report or the archive containing student's
2908 programs.
2909
2910 @returns {string} url
2911
2912*/
2913
2914CodeGradX.Job.prototype.getBaseURL = function () {
2915 const job = this;
2916 const path = job.pathdir + '/' + job.jobid;
2917 return path;
2918};
2919CodeGradX.Job.prototype.getReportURL = function () {
2920 const job = this;
2921 return job.getBaseURL() + '.xml';
2922};
2923CodeGradX.Job.prototype.getProblemReportURL = function () {
2924 const job = this;
2925 return job.getBaseURL() + '_.xml';
2926};
2927CodeGradX.Job.prototype.getTgzURL = function () {
2928 const job = this;
2929 return job.getBaseURL() + '.tgz';
2930};
2931
2932/** Conversion of texts (stems, reports) from XML to HTML.
2933 This function may be modified to accommodate your own desires.
2934*/
2935
2936CodeGradX.xml2html = function (s, options) {
2937 options = Object.assign({}, CodeGradX.xml2html.default, options);
2938 let result = '';
2939 //const mark, totalMark;
2940 let mode = 'default';
2941 let questionCounter = 0, sectionLevel = 0;
2942 // HTML tags to be left as they are:
2943 const htmlTagsRegExp = new RegExp('^(p|pre|code|ul|ol|li|em|it|i|sub|sup|strong|b)$');
2944 // Tags to be converted into DIV:
2945 const divTagsRegExp = new RegExp('^(warning|error|introduction|conclusion|normal|stem|report)$');
2946 // Tags to be converted into SPAN:
2947 const spanTagsRegExp = new RegExp("^(user|machine|lineNumber)$");
2948 // Tags with special hack:
2949 const specialTagRegExp = new RegExp("^(img|a)$");
2950 // Tags to be ignored:
2951 const ignoreTagsRegExp = new RegExp("^(FW4EX|expectations|title|fw4ex)$");
2952 function convertAttributes (attributes) {
2953 let s = '';
2954 _.forIn(attributes, function (value, name) {
2955 s += ' ' + name + '="' + value + '"';
2956 });
2957 return s;
2958 }
2959 const parser = sax.parser(true, {
2960 //trim: true
2961 });
2962 parser.onerror = function (e) {
2963 throw e;
2964 };
2965 const special = {
2966 "'": "&apos;",
2967 '"': "&quot;",
2968 '<': "&lt;",
2969 '>': "&gt;",
2970 '&': "&amp;"
2971 };
2972 parser.ontext= function (text) {
2973 if ( ! mode.match(/ignore/) ) {
2974 let htmltext = '';
2975 const letters = text.split('');
2976 for ( let i=0 ; i<letters.length ; i++ ) {
2977 const ch = letters[i];
2978 if ( special[ch] ) {
2979 htmltext += special[ch];
2980 } else {
2981 htmltext += ch;
2982 }
2983 }
2984 result += htmltext;
2985 }
2986 };
2987 function absolutize (node) {
2988 if ( options.exercise ) {
2989 const pathRegExp = new RegExp('^(./)?(path/.*)$');
2990 if ( node.attributes.src ) {
2991 let matches = node.attributes.src.match(pathRegExp);
2992 if ( matches ) {
2993 node.attributes.src = options.exercise.server +
2994 '/exercisecontent/' +
2995 options.exercise.safecookie +
2996 '/' +
2997 matches[2];
2998 }
2999 }
3000 if ( node.attributes.href ) {
3001 let matches = node.attributes.href.match(pathRegExp);
3002 if ( matches ) {
3003 node.attributes.href = options.exercise.server +
3004 '/exercisecontent/' +
3005 options.exercise.safecookie +
3006 '/' +
3007 matches[2];
3008 }
3009 }
3010 }
3011 const tagname = node.name;
3012 const attributes = convertAttributes(node.attributes);
3013 return '<' + tagname + attributes + ' />';
3014 }
3015 parser.onopentag = function (node) {
3016 const tagname = node.name;
3017 const attributes = convertAttributes(node.attributes);
3018 if ( tagname.match(ignoreTagsRegExp) ) {
3019 mode = 'ignore';
3020 } else if ( tagname.match(htmlTagsRegExp) ) {
3021 result += '<' + tagname + attributes + '>';
3022 } else if ( tagname.match(specialTagRegExp) ) {
3023 result += absolutize(node);
3024 } else if ( tagname.match(spanTagsRegExp) ) {
3025 result += '<span class="fw4ex_' + tagname + '"' + attributes + '>';
3026 } else if ( tagname.match(divTagsRegExp) ) {
3027 result += '<div class="fw4ex_' + tagname + '"' + attributes + '>';
3028 } else if ( tagname.match(/^mark$/) ) {
3029 const markOrig = CodeGradX._str2num(node.attributes.value);
3030 const mark = options.markFactor *
3031 CodeGradX._str2num2decimals(markOrig);
3032 result += '<span' + attributes + ' class="fw4ex_mark">' +
3033 mark + '<!-- ' + markOrig;
3034 } else if ( tagname.match(/^section$/) ) {
3035 result += '<div' + attributes + ' class="fw4ex_section' +
3036 (++sectionLevel) + '">';
3037 } else if ( tagname.match(/^question$/) ) {
3038 const qname = node.attributes.name;
3039 const title = node.attributes.title || '';
3040 result += '<div' + attributes + ' class="fw4ex_question">';
3041 result += '<div class="fw4ex_question_title" data_counter="' +
3042 (++questionCounter) + '">' + qname + ": " +
3043 title + '</div>';
3044 } else {
3045 result += '<div class="fw4ex_' + tagname + '"' + attributes + '>';
3046 }
3047 };
3048 parser.onclosetag = function (tagname) {
3049 if ( tagname.match(ignoreTagsRegExp) ) {
3050 mode = 'default';
3051 } else if ( tagname.match(htmlTagsRegExp) ) {
3052 result += '</' + tagname + '>';
3053 } else if ( tagname.match(specialTagRegExp) ) {
3054 result = result;
3055 } else if ( tagname.match(spanTagsRegExp) ) {
3056 result += '</span>';
3057 } else if ( tagname.match(divTagsRegExp) ) {
3058 result += '</div>';
3059 } else if ( tagname.match(/^mark$/) ) {
3060 result += ' --></span>';
3061 } else if ( tagname.match(/^section$/) ) {
3062 --sectionLevel;
3063 result += '</div>';
3064 } else if ( tagname.match(/^question$/) ) {
3065 result += '</div>';
3066 } else {
3067 result += '</div>';
3068 }
3069 };
3070 parser.oncomment = function (text) {
3071 if ( ! mode.match(/ignore/) ) {
3072 result += '<!-- ' + text + ' -->';
3073 }
3074 };
3075 parser.oncdata = function (text) {
3076 if ( ! mode.match(/ignore/) ) {
3077 result += '<pre>' + he.encode(text);
3078 }
3079 };
3080 parser.cdata = function (text) {
3081 if ( ! mode.match(/ignore/) ) {
3082 result += he.encode(text);
3083 }
3084 };
3085 parser.onclosecdata = function () {
3086 if ( ! mode.match(/ignore/) ) {
3087 result += '</pre>';
3088 }
3089 };
3090 parser.write(s).close();
3091 if ( questionCounter <= 1 ) {
3092 // If only one question, remove its title:
3093 let questionTitleRegExp = new RegExp(
3094 '<div class=.fw4ex_question_title. [^>]*>.*?</div>');
3095 result = result.replace(questionTitleRegExp, '');
3096 }
3097 return result;
3098};
3099CodeGradX.xml2html.default = {
3100 markFactor: 100
3101};
3102
3103// ************************** Batch *************************
3104/** A Batch is a set of students' answers to be marked by a single
3105 Exercise. Instantaneous reports or final reports may be obtained
3106 with the `getReport()` or `getFinalReport()` methods.
3107
3108 @constructor
3109 @property {string} label - name of the batch
3110 @property {number} totaljobs - the total number of students' jobs to mark
3111 @property {number} finishedjobs - the total number of marked students' jobs
3112 @property {Hashtable<Job>} jobs - Hashtable of jobs indexed by their label
3113
3114 */
3115
3116CodeGradX.Batch = function (js) {
3117 Object.assign(this, js);
3118};
3119
3120/** Get the current state of the Batch report that is, always fetch
3121 it. See also `getFinalReport()` to get the final report of the
3122 batch where all answers are marked.
3123
3124 @param {Object} parameters - parameters {@see sendRepeatedlyESServer}
3125 @returns {Promise<Batch>} yielding Batch
3126
3127 */
3128
3129CodeGradX.Batch.prototype.getReport = function (parameters) {
3130 const batch = this;
3131 const state = CodeGradX.getCurrentState();
3132 state.debug('getBatchReport1', batch);
3133 parameters = Object.assign({
3134 // So progress() may look at the current version of the batch report:
3135 batch: batch
3136 },
3137 CodeGradX.Batch.prototype.getReport.default,
3138 parameters);
3139 const path = batch.getReportURL();
3140 function processResponse (response) {
3141 //console.log(response);
3142 state.debug('getBatchReport2', response, batch);
3143 batch.originServer = response.url.replace(/^(.*)\/s\/.*$/, "$1");
3144 function processJS (js) {
3145 //console.log(js);
3146 state.debug('getBatchReport3', js);
3147 js = js.fw4ex.multiJobStudentReport;
3148 batch.totaljobs = CodeGradX._str2num(js.$.totaljobs);
3149 batch.finishedjobs = CodeGradX._str2num(js.$.finishedjobs);
3150 batch.jobs = {};
3151 //console.log(js);
3152 function processJob (jsjob) {
3153 //console.log(jsjob);
3154 let job = state.cache.jobs[jsjob.$.jobid];
3155 if ( ! job ) {
3156 job = new CodeGradX.Job({
3157 exercise: batch.exercise,
3158 XMLjob: jsjob,
3159 jobid: jsjob.$.jobid,
3160 pathdir: jsjob.$.location,
3161 label: jsjob.$.label,
3162 problem: false,
3163 mark: CodeGradX._str2num2decimals(
3164 jsjob.marking.$.mark),
3165 totalMark: CodeGradX._str2num2decimals(
3166 jsjob.marking.$.totalMark),
3167 started: CodeGradX._str2Date(jsjob.marking.$.started),
3168 finished: CodeGradX._str2Date(jsjob.marking.$.finished)
3169 });
3170 if ( jsjob.$.problem ) {
3171 job.problem = true;
3172 }
3173 job.duration = (job.finished.getTime() -
3174 job.started.getTime() )/1000; // seconds
3175 state.cache.jobs[job.jobid] = job;
3176 }
3177 batch.jobs[job.label] = job;
3178 return job;
3179 }
3180 if ( js.jobStudentReport ) {
3181 if ( Array.isArray(js.jobStudentReport) ) {
3182 js.jobStudentReport.forEach(processJob);
3183 } else {
3184 processJob(js.jobStudentReport);
3185 }
3186 }
3187 return when(batch);
3188 }
3189 batch.XMLreport = response.entity;
3190 return CodeGradX.parsexml(response.entity)
3191 .then(processJS)
3192 .catch(function (reason) {
3193 /* eslint "no-console": 0 */
3194 console.log(reason);
3195 console.log(response);
3196 return when.reject(reason);
3197 });
3198 }
3199 return state.sendRepeatedlyESServer('s', parameters, {
3200 path: path,
3201 method: 'GET',
3202 headers: {
3203 "Accept": "text/xml"
3204 }
3205 }).then(processResponse);
3206};
3207CodeGradX.Batch.prototype.getReport.default = {
3208 step: 5, // seconds
3209 attempts: 100,
3210 progress: function (/*parameters*/) {}
3211};
3212
3213/** Get the final state of the Batch report where all
3214 answers are marked. This method will update the `finishedjobs`
3215 and `jobs` fields.
3216
3217 @param {Object} parameters - parameters {@see sendRepeatedlyESServer}
3218 @returns {Promise<Batch>} yielding Batch
3219
3220 */
3221
3222CodeGradX.Batch.prototype.getFinalReport = function (parameters) {
3223 const batch = this;
3224 const state = CodeGradX.getCurrentState();
3225 state.debug('getBatchFinalReport1', batch);
3226 if ( batch.finishedjobs &&
3227 batch.finishedjobs === batch.totaljobs ) {
3228 // Only return a complete report
3229 return when(batch);
3230 }
3231 parameters = Object.assign({
3232 // So progress() may look at the current version of the batch report:
3233 batch: batch
3234 },
3235 CodeGradX.Batch.prototype.getReport.default,
3236 parameters);
3237 if ( parameters.step < CodeGradX.Batch.prototype.getReport.default.step ) {
3238 parameters.step = CodeGradX.Batch.prototype.getReport.default.step;
3239 }
3240 function tryFetching () {
3241 state.debug('getBatchFinalReport3', parameters);
3242 // Get at least one report to access finishedjobs and totaljobs:
3243 return batch.getReport(parameters).then(fetchAgainReport);
3244 }
3245 function fetchAgainReport () {
3246 state.debug('getBatchFinalReport2', batch);
3247 if ( batch.finishedjobs < batch.totaljobs ) {
3248 const dt = parameters.step * 1000; // seconds
3249 return when(batch).delay(dt).then(tryFetching);
3250 } else {
3251 return when(batch);
3252 }
3253 }
3254 return tryFetching();
3255};
3256
3257CodeGradX.Batch.prototype.getReportURL = function () {
3258 const batch = this;
3259 if ( ! batch.pathdir ) {
3260 batch.pathdir = '/b' +
3261 batch.batchid.replace(/-/g, '').replace(/(.)/g, "/$1");
3262 }
3263 const path = batch.pathdir + '/' + batch.batchid + '.xml';
3264 return path;
3265};
3266
3267// end of codegradxlib.js