diff options
author | Marko Pušnik <marko.pusnik@guru.si> | 2015-09-30 19:07:37 +0200 |
---|---|---|
committer | Marko Pušnik <marko.pusnik@guru.si> | 2015-09-30 19:07:37 +0200 |
commit | 8bb68a1e979022ead00535c25403b341f2cb24bb (patch) | |
tree | 0ad16cf9acb81cbb6f8484f32fed657e8c66dabb | |
parent | ba6135a83273c625097fe7cdb59319a51acca31e (diff) | |
parent | 0da1117cfc28688633be7b8382aa60435bf740eb (diff) |
Merge branch 'master' of odie.guru.si:codeq-web
Conflicts:
index.html
-rw-r--r-- | index.html | 62 | ||||
-rw-r--r-- | js/codeq/comms.js | 14 | ||||
-rw-r--r-- | js/codeq/core.js | 63 | ||||
-rw-r--r-- | js/codeq/hint.js | 238 | ||||
-rw-r--r-- | js/codeq/language.js | 8 | ||||
-rw-r--r-- | js/codeq/login.js | 4 | ||||
-rw-r--r-- | js/codeq/navigation.js | 6 | ||||
-rw-r--r-- | js/codeq/problem.js | 42 | ||||
-rw-r--r-- | js/codeq/python.js | 2 | ||||
-rw-r--r-- | js/codeq/robot.js | 300 | ||||
-rw-r--r-- | js/codeq/translation.js | 9 | ||||
-rw-r--r-- | res/en.json | 15 | ||||
-rw-r--r-- | res/sl.json | 15 |
13 files changed, 688 insertions, 90 deletions
@@ -36,10 +36,11 @@ <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> - <li style="display: none;" id="navigation-language"><a href="">Language</a></li> - <li style="display: none;" id="navigation-problem"><a href="">Problem</a></li> - <li style="display: none;" id="navigation-python"><a href="">Python</a></li> - <li style="display: none;" id="navigation-prolog"><a href="">Prolog</a></li> + <li style="display: none;" id="navigation-language"><a href="" data-tkey="language">Language</a></li> + <li style="display: none;" id="navigation-problem"><a href="" data-tkey="problem">Problem</a></li> + <li style="display: none;" id="navigation-python"><a href="" data-tkey="python">Python</a></li> + <li style="display: none;" id="navigation-prolog"><a href="" data-tkey="prolog">Prolog</a></li> + <li style="display: none;" id="navigation-robot"><a href="" data-tkey="robot">Robot</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <p class="navbar-text" id="signed-in-title"></p> @@ -54,10 +55,10 @@ <span class="glyphicon glyphicon glyphicon-user"></span> </a> <ul class="dropdown-menu"> - <li id="navigation-logout"><a href="#">Logout</a></li> - <li id="navigation-profile"><a href="#">Profile</a></li> + <li id="navigation-logout"><a href="#" data-tkey="logout">Logout</a></li> + <li id="navigation-profile"><a href="#" data-tkey="profile">Profile</a></li> <li role="separator" class="divider"></li> - <li><a href="#" data-toggle="modal" data-target="#modalChangePassword">Change password</a></li> + <li><a href="#" data-toggle="modal" data-target="#modalChangePassword" data-tkey="change_pass">Change password</a></li> </ul> </li> <li class="dropdown"> @@ -65,7 +66,7 @@ <div class="dropdown-menu" style="background-color: #fff"> <form class="form col-sm-12"> <div class="form-group"> - <span class="small">Language</span> + <span class="small" data-tkey="language">Language</span> <a class="text-muted" data-container="body" data-toggle="popover" data-trigger="hover" data-placement="left" data-html="true" data-content="<span class='small'>Select the UI language.</span>" data-original-title="" title=""><i class="glyphicon glyphicon-question-sign"></i></a><br> <select class="form-control"> <option value="sl">Slovensko</option> @@ -74,7 +75,7 @@ </select> </div> <div class="form-group"> - <span class="small"> + <span class="small" data-tkey="language"> Language </span> <a class="text-muted" data-container="body" data-toggle="popover" data-trigger="hover" data-placement="left" data-html="true" data-content="<span class='small'>Select the UI language.</span>" data-original-title="" title=""><i class="glyphicon glyphicon-question-sign"></i></a><br> @@ -164,9 +165,9 @@ <div class="col-lg-3 col-md-6 col-sm-12 block block2" style="padding-top: 51px; padding-bottom: 21px;"> <nav class="navbar navbar-default block-toolbar"> <div class="container-fluid"> - <button type="button" class="btn btn-default navbar-btn btn-plan">Plan</button> - <button type="button" class="btn btn-default navbar-btn btn-hint">Hint</button> - <button type="button" class="btn btn-default navbar-btn btn-test">Test</button> + <button type="button" class="btn btn-default navbar-btn btn-plan" data-tkey="btn_plan">Plan</button> + <button type="button" class="btn btn-default navbar-btn btn-hint" data-tkey="btn_hint">Hint</button> + <button type="button" class="btn btn-default navbar-btn btn-test" data-tkey="btn_test">Test</button> </div> </nav> <div class="code_editor"></div> @@ -195,10 +196,42 @@ <div class="col-lg-3 col-md-6 col-sm-12 block block2" style="padding-top: 51px; padding-bottom: 21px;"> <nav class="navbar navbar-default block-toolbar"> <div class="container-fluid"> + <button type="button" class="btn btn-default navbar-btn btn-plan" data-tkey="btn_plan">Plan</button> + <button type="button" class="btn btn-default navbar-btn btn-hint" data-tkey="btn_hint">Hint</button> + <button type="button" class="btn btn-default navbar-btn btn-test" data-tkey="btn_test">Test</button> + <button type="button" class="btn btn-default navbar-btn btn-run" data-tkey="btn_run">Run</button> + <button type="button" class="btn btn-default navbar-btn btn-stop" data-tkey="btn_stop">Stop</button> + </div> + </nav> + <div class="code_editor"></div> + <div class="block-label">Code</div> + </div> + <div class="col-lg-3 col-md-6 col-sm-12 block block3"> + <div class="console"></div> + <div class="block-label">Console</div> + </div> + <div class="col-lg-3 col-md-6 col-sm-12 block block4"> + <div class="hints"></div> + <div class="block-label">Hints</div> + </div> + </div><!--/row--> + </div><!--container--> + + <!-- problem screen: robot --> + <div class="container-fluid quadrants block1" id="screen_robot" style="display: none;"> + <div class="row"> + <div class="col-lg-3 col-md-6 col-sm-12 block block1"> + <h2 class="title translatable" data-dict="robot" data-tkey="slug"></h2> + <div class="description translatable" data-dict="robot" data-tkey="description"></div> + <div class="block-label">Instructions</div> + </div> + <div class="col-lg-3 col-md-6 col-sm-12 block block2"> + <nav class="navbar navbar-default"> + <div class="container-fluid"> <button type="button" class="btn btn-default navbar-btn btn-plan">Plan</button> <button type="button" class="btn btn-default navbar-btn btn-hint">Hint</button> - <button type="button" class="btn btn-default navbar-btn btn-test">Test</button> <button type="button" class="btn btn-default navbar-btn btn-run">Run</button> + <input type="text" id="robot_ip" placeholder="Robot's IP"></input> <button type="button" class="btn btn-default navbar-btn btn-stop">Stop</button> </div> </nav> @@ -222,7 +255,7 @@ <h2> Profile <div class="btn-group btn-group-xs hidden-md pull-right"> - <a href="" data-toggle="modal" data-target="#modalChangePassword" class="btn btn-default">Change Password</a> + <a href="" data-toggle="modal" data-target="#modalChangePassword" class="btn btn-default" data-tkey="change_pass">Change Password</a> <a href="#" class="btn btn-default" id="btnProfileGoBack">Go back</a> </div> </h2> @@ -438,6 +471,7 @@ <script src="js/codeq/hint.js"></script> <script src="js/codeq/prolog.js"></script> <script src="js/codeq/python.js"></script> + <script src="js/codeq/robot.js"></script> <script src="js/codeq/login.js"></script> <script src="js/codeq/profile.js"></script> <script src="js/codeq/language.js"></script> diff --git a/js/codeq/comms.js b/js/codeq/comms.js index ecca6ff..f416e88 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -165,7 +165,6 @@ // ================================================================================ var languageCache = {}, // language defs, keyed by language identifier - problemCache = {}, // problem cache, 3-level, keyed by: language, problem group, and problem identifier ajaxGet = function (url) { return Q.Promise(function (resolve, reject, notify) { $.ajax({ @@ -197,7 +196,7 @@ }); }); }, - ajaxPrefix; + ajaxPrefix, ajaxDataPrefix, ajaxResPrefix; ajaxPrefix = location.pathname; if (!ajaxPrefix) ajaxPrefix = '/'; @@ -206,7 +205,8 @@ ajaxPrefix[ajaxPrefix.length - 1] = ''; ajaxPrefix = ajaxPrefix.join('/'); } - ajaxPrefix = ajaxPrefix + 'data/'; + ajaxDataPrefix = ajaxPrefix + 'data/'; + ajaxResPrefix = ajaxPrefix + 'res/'; // ================================================================================ // This module's API methods @@ -399,7 +399,7 @@ return Q(x); } } - x = ajaxGet(ajaxPrefix + identifier + '/language.json').then( + x = ajaxGet(ajaxDataPrefix + identifier + '/language.json').then( function getLanguageDefSuccess(data) { languageCache[identifier] = data; return data; // proxy further @@ -414,7 +414,11 @@ }, 'getProblemDef': function (language, group, problem) { - return ajaxGet(ajaxPrefix + language + '/' + group + '/' + problem + '/problem.json'); + return ajaxGet(ajaxDataPrefix + language + '/' + group + '/' + problem + '/problem.json'); + }, + + 'getGuiTranslation': function (lang) { + return ajaxGet(ajaxResPrefix + lang + '.json'); } }; })(); diff --git a/js/codeq/core.js b/js/codeq/core.js index 01382d1..29b497c 100644 --- a/js/codeq/core.js +++ b/js/codeq/core.js @@ -372,6 +372,49 @@ // The boot sequence // ================================================================================ + var loadGuiTranslations = function () { + var langs = codeq.availableLangs, + loaders = [], + loaderLangs = [], + i, lang; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + loaders.push(codeq.comms.getGuiTranslation(lang)); + loaderLangs.push(lang); + } + return Q.all(loaders) + .then(function (results) { + // convert translations into something that the translation module can use + var dictionary = {}, + i, json, key, lang, translations; + for (i = results.length - 1; i >= 0; i--) { + json = results[i]; + lang = loaderLangs[i]; + for (key in json) { + if (!json.hasOwnProperty(key)) continue; + translations = dictionary[key]; + if (translations) translations[lang] = json[key]; + else { + translations = {}; + dictionary[key] = translations; + translations[lang] = json[key]; + } + } + } + // ensure each key contains all translations + for (key in dictionary) { + if (!dictionary.hasOwnProperty(key)) continue; + translations = dictionary[key]; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + if (!(lang in translations)) translations[lang] = "(Untranslated: " + lang + ")"; + } + } + // register with the system + codeq.tr.registerDictionary('gui', dictionary); + }); + }; + $(document).ready(function () { // set the language var navigatorLang = navigator.language || navigator.browserLanguage, // language reported by browser @@ -387,11 +430,17 @@ codeq.availableLangs.push(key); } - codeq.fire('init'); // tell any interested modules that we are not initialized, perhaps they want to initialize too - codeq.setLang(lang || 'en'); // initial language setting - // go to login - codeq.globalStateMachine.transition('login'); - - //For performance reasons, the Tooltip and Popover data-apis are opt-in, meaning you must initialize them yourself. - $('[data-toggle="popover"]').popover()}); + // the boot chain: must be a sequence of .then() terminated with .done() + loadGuiTranslations() + .then(function () { + codeq.fire('init'); // tell any interested modules that we are now initialized, perhaps they want to initialize too + codeq.setLang(lang || 'en'); // initial language setting, this also translates the GUI + // go to login + codeq.globalStateMachine.transition('login'); + + //For performance reasons, the Tooltip and Popover data-apis are opt-in, meaning you must initialize them yourself. + $('[data-toggle="popover"]').popover() + }) + .done(); + }); })(); diff --git a/js/codeq/hint.js b/js/codeq/hint.js index d30f934..6f54717 100644 --- a/js/codeq/hint.js +++ b/js/codeq/hint.js @@ -12,6 +12,7 @@ hintCleaners = [], planIdx = 0, dictionary = [], + jqHintsContainer = jqHints.parent(), clearHints = function () { var i; @@ -44,45 +45,192 @@ }); }, - typeHandlers = { - 'static': function (type, template, serverHint) { - var args = serverHint ? serverHint.args : null, - jqContainer, jqButton, i, N, tmpl, tmplIsObject; - if (template instanceof Array) { // unwrap the template if there's only one - if (template.length == 0) template = ''; - else if (template.length == 1) template = template[0] + ''; // it must be a string + prepareStaticHintContent = function (hintContent, indices, hintId) { + var content = {}, + langs = codeq.availableLangs, + skippedLangs = [], + Nhints = 0, // count hints and remember the count + haveIndices = indices instanceof Array, + i, lang, j, node, tmpContent; + langLoop: + for (j = langs.length - 1; j >= 0; j--) { + lang = langs[j]; + node = hintContent[lang]; + if (!node) { + // no translation for this language, mark for later + skippedLangs.push(lang); + } + else { + if (haveIndices) { + // traverse node's indices + for (i = 0; i < indices.length; i++) { + node = node[indices[i]]; + if (!node) { + // index out of bounds, mark this language for later + codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + indices); + skippedLangs.push(lang); + continue langLoop; + } + } + } + if (typeof node === 'string') { + // we have a single string (= a simplex message), put it into an array + content[lang] = [node]; + if (Nhints < 1) Nhints = 1; + } + else if (node instanceof Array) { + // we already have an array + if (node.length == 0) { + if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' for language ' + lang + ' contains an empty array'); + else codeq.log.error('Hint ' + hintId + ' for language ' + lang + ' contains an empty array'); + skippedLangs.push(lang); + continue; + } + // verify that each array element is a string or an object with a message + for (i = node.length - 1; i >= 0; i--) { + tmpContent = node[i]; + if (typeof tmpContent === 'string') { + // this is okay + } + else if (tmpContent && (typeof tmpContent === 'object')) { + if (typeof tmpContent.message !== 'string') { + if (haveIndices) tmpContent.message = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i; + else tmpContent.message = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i; + codeq.log.error(tmpContent.message); + } + } + else { + // not a string or an object with a message + if (haveIndices) tmpContent = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i; + else tmpContent = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i; + node[i] = tmpContent; + codeq.log.error(tmpContent); + } + } + content[lang] = node; + if (Nhints < node.length) Nhints = node.length; + } + else if (node && (typeof node === 'object') && (typeof node.message === 'string')) { + // we have a single object with a message (= a complex message), put it into an array + content[lang] = [node]; + if (Nhints < 1) Nhints = 1; + } + else { + if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' did not result in a terminal node for language ' + lang + ', but: ' + node); + else codeq.log.error('Hint ' + hintId + ' probably needs indices because it does not have a terminal node for language ' + lang + ', but: ' + node); + skippedLangs.push(lang); + } } - if (template instanceof Array) { - codeq.log.debug('Processing an array of static hints'); + } + if (Nhints === 0) { + // provide error feedback on display when there is no hint translation available in any language + if (haveIndices) tmpContent = ['No hints found for hint ' + hintId + ' with indices ' + indices]; + else tmpContent = ['No hints found for hint ' + hintId]; + codeq.log.error(tmpContent[0]); + for (j = langs.length - 1; j >= 0; j--) { + content[langs[j]] = tmpContent; + } + Nhints = 1; + } + else if (skippedLangs.length > 0) { + // choose a default content and assign it to skipped languages + lang = 'en'; // try English first + tmpContent = content[lang]; + if (!tmpContent) { + // if no English exists, find one that does + for (lang in content) { + if (!content.hasOwnProperty(lang)) continue; + tmpContent = content[lang]; + if (tmpContent) break; + } + } + codeq.log.error('Translations in languages ' + skippedLangs.join(', ') + ' are missing or erroneous for hint ' + hintId + ', replacing their content with translation for ' + lang); + // assign the default content to skipped languages + for (j = skippedLangs.length - 1; j >= 0; j--) { + content[skippedLangs[j]] = tmpContent; + } + } + content.hintLength = Nhints; + return content; + }, + + ta = function (trObj) { // an object of the form: {'en': 'english content', 'sl': 'slovenska vsebina'} + var result = ['data-dict="', trNamespace, '" data-tkey="', dictionary.length, '"'].join(''); + dictionary.push(trObj); + return result; + }, + + typeHandlers = { + 'static': function (type, template, serverHint, hintId) { + var content = prepareStaticHintContent(template, serverHint.indices, hintId), + args = serverHint ? serverHint.args : null, + hintIndex = 0, + trButton = {}, + Nhints = content.hintLength, + nextJqHint = function () { + var trContent = {}, + langs = codeq.availableLangs, + lang, i, msg, jq, deltaHeight; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + try { + msg = content[lang][hintIndex]; + if (typeof msg === 'string') { + trContent[lang] = processTemplate(msg, args); + trButton[lang] = 'More...'; + } + else { + trContent[lang] = processTemplate(msg.message, args); + trButton[lang] = msg.linkText; + } + } + catch (e) { + msg = 'Error processing hint ' + hintId + ' at index ' + hintIndex + ' for language ' + lang + ': ' + e; + codeq.log.error(msg, e); + trContent[lang] = msg; + } + } + jq = $('<div class="hint-static translatable" ' + ta(trContent) + '></div>'); + hintIndex++; + if (jqButton) { + if (hintIndex < Nhints) { + jqButton.before(jq); + codeq.tr.translateDom(jqButton); + } + else { + jqButton.remove(); + jqContainer.append(jq); + } + } + else { + jqContainer.append(jq); + } + codeq.tr.translateDom(jq); + // scroll into view if overflowing + deltaHeight = jqHints.height() - jqHintsContainer.height(); + if (deltaHeight > 0) { + jqHintsContainer.scrollTop(deltaHeight); + } + }, + jqContainer, jqButton; + + if (Nhints > 1) { + // hint sequence jqContainer = $('<div class="hint-static-group"></div>'); - jqButton = $('<a class="hint-static-link"></a>'); + jqButton = $('<a class="hint-static-link translatable" ' + ta(trButton) + '></a>'); jqHints.append(jqContainer); - N = template.length; - tmpl = template[0]; - tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null); - jqContainer.append('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>'); jqContainer.append(jqButton); - jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" - i = 1; jqButton.on('click', function () { - var tmpl = template[i], - tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null), - jqNext = $('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>'); - i++; - if (i < N) { - jqButton.before(jqNext); - jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" - } - else { - jqButton.remove(); - jqContainer.append(jqNext); - } + nextJqHint(); }); } else { - codeq.log.debug('Processing a single static hint'); - jqHints.append('<div class="hint-static">' + processTemplate(template, args) + '</div>'); + // a single hint + jqContainer = jqHints; + jqButton = null; } + nextJqHint(); + // no hint cleaner here, a static hint remains on the screen }, @@ -137,7 +285,7 @@ hintCommonDefs = commonDef.hint_type, hintProblemTr = problemDef.hint, hintCommonTr = commonDef.hint, - planDef = problemDef.plan.sl; + planDef = problemDef.plan; return { /** @@ -146,12 +294,16 @@ */ 'planNext': function () { if (planIdx < planDef.length) { - typeHandlers['static']('static', planDef[planIdx], null); + typeHandlers.static('static', planDef[planIdx], {}, 'plan'); planIdx++; } return planIdx < planDef.length; }, + 'hasNextPlan': function () { + return planIdx < planDef.length; + }, + /** * Processes and display appropriately the server hints. * TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last @@ -160,7 +312,8 @@ */ 'handle': function (serverHints) { var n = serverHints.length, - i, serverHint, hintId, hintDef, hintContent, hintType, hintTemplate, t, fn, indices; + i, serverHint, hintId, hintDef, hintContent, hintType, t, fn, indices; + clearHints(); mainLoop: for (i = 0; i < n; i++) { @@ -182,28 +335,18 @@ codeq.log.error('Hint without content: ' + hintId); continue; } - if (serverHint.indices) { - indices = serverHint.indices; - for (i = 0; i < indices.length; i++) { - hintContent = hintContent[indices[i]]; - if (!hintContent) { - codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + serverHint.indices); - continue mainLoop; - } - } - } t = typeof hintDef; - if (t === 'string') hintType = hintDef; - else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; + if (t === 'string') hintType = hintDef; // currently a hint type is a string + else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; // but in future we may use an object, if a definition becomes more complex else { - codeq.log.error('Cannot determine type of hint ' + hintId + ' from: ' + hintDef); + codeq.log.error('Cannot determine the type of hint ' + hintId + ' from: ' + hintDef); continue; } fn = typeHandlers[hintType]; if (!fn) codeq.log.error('Unsupported hint type: ' + hintType); - else fn(hintType, hintContent.sl, serverHint); + else fn(hintType, hintContent, serverHint, hintId); } }, @@ -213,6 +356,7 @@ jqHints.empty(); jqHints = null; jqEditor = null; + jqHintsContainer = null; editor = null; } }; diff --git a/js/codeq/language.js b/js/codeq/language.js index aac4e78..0893e33 100644 --- a/js/codeq/language.js +++ b/js/codeq/language.js @@ -6,8 +6,10 @@ var jqScreen = $('#screen_language'),
jqProlog = $('#choose-prolog'),
jqPython = $('#choose-python'),
+ jqRobot = $('#choose-robot'),
chooseProlog = function () {codeq.globalStateMachine.transition('problem', 'prolog');},
- choosePython = function () {codeq.globalStateMachine.transition('problem', 'python');};
+ choosePython = function () {codeq.globalStateMachine.transition('problem', 'python');},
+ chooseRobot = function () {codeq.globalStateMachine.transition('problem', 'robot');};
codeq.globalStateMachine.register('language',{
'enter': function(){
@@ -16,13 +18,15 @@ jqScreen.css('display', '');
jqProlog.on('click', chooseProlog);
jqPython.on('click', choosePython);
+ jqRobot.on('click', chooseRobot);
},
'exit' : function(){
jqProlog.off();
jqPython.off();
+ jqRobot.off();
jqScreen.css('display', 'none');
$('#navigation-language').css('display', 'none').removeClass("active");
}
});
-})();
\ No newline at end of file +})();
diff --git a/js/codeq/login.js b/js/codeq/login.js index e5a446b..7844dc4 100644 --- a/js/codeq/login.js +++ b/js/codeq/login.js @@ -16,9 +16,9 @@ if (data.code !== 0) throw new Error('Login failed, code: ' + data.code + ', message: ' + data.message); //merge these settings into the already existing default settings - sett = data.settings; + var sett = data.settings; $.merge(true, codeq.settings, sett); - if('lang' in sett && sett['lan'] in codeq.supportedLangs){ + if('lan' in sett && sett['lan'] in codeq.supportedLangs){ codeq.setLang(sett['lan']); } diff --git a/js/codeq/navigation.js b/js/codeq/navigation.js index 80fd321..4eff834 100644 --- a/js/codeq/navigation.js +++ b/js/codeq/navigation.js @@ -97,6 +97,10 @@ codeq.globalStateMachine.transition('prolog'); e.preventDefault(); }); + $('#navigation-robot').on('click', function(e){ + codeq.globalStateMachine.transition('robot'); + e.preventDefault(); + }); $('#navigation-logout').on('click', function(e){ codeq.globalStateMachine.transition('login'); e.preventDefault();//prevent this since we'll trigger a page reload otherwise @@ -106,4 +110,4 @@ e.preventDefault();//prevent this since we'll trigger a page reload otherwise }); -})();
\ No newline at end of file +})(); diff --git a/js/codeq/problem.js b/js/codeq/problem.js index d07e5fa..e3de85a 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -83,25 +83,51 @@ if (!translation || !(translation instanceof Array)) return false; return translation.length > 0; }, + /** + * Structurally converts the plan definition into something that the + * hint module can work with. + * The input is the translations object from problem.json in the form + * {'sl': { ..., 'plan': [hint1_sl, hint2_sl, ...]}, 'en': { ..., 'plan': [hint1_en, hint2_en, ...]}, ...} + * The output is a list of hints in the plan, translations are in each hint: + * [{'sl': hint1_sl, 'en': hint1_en, ...}, {'sl': hint2_sl, 'en': hint2_en}, ...] + */ processPlans = function (rawTranslations) { // find the default plan translation var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan', defaultPlanCondition) || [], - allPlans = {}, // the result - i, lang, tr; - // create all translations for plan + result = [], + i, lang, tr, plan, j, fragment; + if (defaultPlan.length == 0) return result; // no plan + // copy all translations from plan for (i = langs.length - 1; i >= 0; i--) { lang = langs[i]; tr = rawTranslations[lang]; // set up plan if (tr && defaultPlanCondition(tr.plan)) { - allPlans[lang] = tr.plan; + plan = tr.plan; } else { // there's no plan in the current language, copy the default plan - allPlans[lang] = defaultPlan; + plan = defaultPlan; + } + if (!(plan instanceof Array)) plan = [plan]; + for (j = 0; j < plan.length; j++) { + if (j < result.length) fragment = result[j]; + else { + fragment = {}; + result.push(fragment); + } + fragment[lang] = plan[j]; } } - return allPlans; + // ensure each plan element has all translations + for (j = result.length - 1; j >= 0; j--) { + fragment = result[j]; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + if (!fragment[lang]) fragment[lang] = 'Missing plan for language ' + lang + ' at index ' + j; + } + } + return result; }, // ================================================================================ @@ -240,7 +266,7 @@ .spread(function (userProblemData, generalProblemData) { if (userProblemData.code !== 0) throw new Error('Failed to obtain user problem data, code: ' + userProblemData.code + ', message: ' + userProblemData.message); if (!generalProblemData) throw new Error('General problem data is not defined'); - codeq.globalStateMachine.transition(language, generalProblemData, data.commonDef, userProblemData.solution); + codeq.globalStateMachine.transition(language, generalProblemData, data.commonDef, userProblemData.data.solution); }) ) .fail(function (reason) { @@ -356,4 +382,4 @@ $('#navigation-problem').css('display', 'none').removeClass("active"); } }); -})();
\ No newline at end of file +})(); diff --git a/js/codeq/python.js b/js/codeq/python.js index 8a9e92f..59e010a 100644 --- a/js/codeq/python.js +++ b/js/codeq/python.js @@ -233,7 +233,7 @@ if (currentSolution) editor.setValue(currentSolution); // $('#screen_python .title').text(problem.slug); // jqDescriptionContent.html(problem.description); - jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.sl) || []).length == 0); + jqBtnPlan.prop('disabled', !hinter.hasNextPlan()); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), diff --git a/js/codeq/robot.js b/js/codeq/robot.js new file mode 100644 index 0000000..f2f4a9b --- /dev/null +++ b/js/codeq/robot.js @@ -0,0 +1,300 @@ +/** + * The robot state of the state machine. When it is entered it'll prepare the code editor and load a sub-state machine which represents the 3 different parts of the screen. + */ + +(function() { + var subScreens, //this will be the actual (sub)state machine + stateNameTag = 'stateName', //a tag for data which is added to some html elements + jqScreen = $('#screen_robot'), // the screen container element + //quadrants + jqDescription = jqScreen.find('.block1'), + jqCode = jqScreen.find('.block2'), + jqConsole = jqScreen.find('.block3'), + jqInfo = jqScreen.find('.block4'), + jqAllQuadrants = jqDescription.add(jqCode).add(jqConsole).add(jqInfo), // all the quadrants + // buttons + jqBtnPlan = jqScreen.find('.btn-plan'), + jqBtnHint = jqScreen.find('.btn-hint'), + jqBtnRun = jqScreen.find('.btn-run'), + jqBtnStop = jqScreen.find('.btn-stop'), + jqInfoButtons = jqBtnPlan.add(jqBtnHint), // all info-focusing buttons + jqAllButtons = jqInfoButtons.add(jqBtnRun).add(jqBtnStop), // all buttons + // misc + currentSubState = null, + transitionEventName = 'mousedown',//event name of the event which will trigger the transition between these substates - the most common transition at least (there are some corner cases on the hint and test buttons -> see the code below) + substates = { + 'description': { + 'enter': function () { + currentSubState = 'block1'; + jqScreen.addClass(currentSubState); + }, + 'exit': function () { + jqScreen.removeClass(currentSubState); + currentSubState = null; + } + }, + 'code': { + 'enter': function () { + currentSubState = 'block2'; + jqScreen.addClass(currentSubState); + }, + 'exit': function () { + jqScreen.removeClass(currentSubState); + currentSubState = null; + } + }, + 'info': { + 'enter': function () { + currentSubState = 'block4'; + jqScreen.addClass(currentSubState); + }, + 'exit': function () { + jqScreen.removeClass(currentSubState); + currentSubState = null; + } + }, + 'console': { + 'enter': function () { + currentSubState = 'block3'; + jqScreen.addClass(currentSubState); + }, + 'exit': function () { + jqScreen.removeClass(currentSubState); + currentSubState = null; + } + } + }; + var robotHandler; //created when we enter the robot state and destroyed once we leave it + codeq.globalStateMachine.register('robot', { + 'enter': function (problemDef, commonDef, currentSolution) { + $('#navigation-language').css('display', ''); + $('#navigation-problem').css('display', ''); + $("#navigation-robot").addClass("active"); + $('#navigation-robot').css('display', ''); + + jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly + robotHandler = createRobotHandler(problemDef, commonDef, currentSolution); + subScreens = codeq.makeStateMachine(substates); + subScreens.transition(jqDescription.data(stateNameTag)); +/* Q.delay(100).then(function(){ + jqAllQuadrants.addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading + }).done();*/ + jqInfoButtons.on(transitionEventName, function (event) { + subScreens.transition('info'); // set focus on the hints quadrant + event.stopPropagation(); // don't allow the event to go on and trigger further transition + }); + jqBtnRun.on(transitionEventName, function (event) { + subScreens.transition('console'); // set focus on the hints quadrant + event.stopPropagation(); // don't allow the event to go on and trigger further transition + }); + + jqAllQuadrants.on(transitionEventName, function () { + subScreens.transition($(this).data(stateNameTag)); + }); + }, + 'exit': function () { + jqAllButtons.off(); // unregister all event handlers + jqAllQuadrants.off(); + jqScreen.css('display', 'none'); +// jqAllQuadrants.removeClass('transition'); + robotHandler.destroy(); + robotHandler = null; + subScreens.destroy(); + subScreens = null; + jqScreen.addClass('block1'); + + $('#navigation-language').css('display', 'none'); + $('#navigation-problem').css('display', 'none'); + $("#navigation-robot").removeClass("active"); + $('#navigation-robot').css('display', 'none'); + } + }); + + jqDescription.data(stateNameTag, 'description'); + jqCode.data(stateNameTag, 'code'); + jqConsole.data(stateNameTag, 'console'); + jqInfo.data(stateNameTag, 'info'); + + // a constant + var firstCharacterPos = {'line': 0, 'ch': 0}; + + var makeRobotTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) { + var terminal = codeq.makeConsole(jqConsole, { + 'greeting': 'CodeQ Robot terminal proxy', + 'autoHistory': true + }); + + terminal.onInput = function (text) { + terminal.append('Not implemented.\n', 'output'); + }; + return terminal; + }; + + var makeActivityHandler = function (editor, problem_id) { + var lastActivityMillis = Date.now(), + deltaActivityMillis = function deltaActivityMillisFunc () { + var now = Date.now(), + dt = now - lastActivityMillis; + lastActivityMillis = now; + return dt; + }, + queue = [], + ts = null, + timer = function () { + var promise; + ts = null; + if (queue.length === 0) return Q(true); + promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id); + queue.length = 0; + return promise; + }, + flush = function () { + clearTimeout(ts); + return timer(); + }; + + return { + 'queueTrace': function (trace) { + trace['dt'] = deltaActivityMillis(); + queue.push(trace); + if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds + return this; + }, + 'flush': flush, + 'addAndPurge': function (trace) { + var accumulatedTrace = queue; + queue = []; + trace['dt'] = deltaActivityMillis(); + accumulatedTrace.push(trace); + if (ts !== null) { + clearTimeout(ts); + ts = null; + } + return accumulatedTrace; + } + }; + }; + + codeq.on('init', function (args) { + codeq.tr.registerDictionary('robot', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active + }); + + /** + * Creates a new handler for the given Prolog assignment definition. + * + * @param {PrologTaskDef} info + * @returns {{destroy: Function, processServerHints: Function}} + */ + var createRobotHandler = function (problemDef, commonDef, currentSolution) { + var //problem = info.problem, + jqDescriptionContent = jqDescription.find('.description'), + jqEditor = jqCode.find('.code_editor'), + jqTerminal = jqConsole.find('.console'), + jqHints = jqInfo.find('.hints'), + editor = codeq.makeEditor(jqEditor[0], { + mode: 'python', + indentUnit: 4 + }), + activityHandler = makeActivityHandler(editor, problemDef.id), + terminal = makeRobotTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), + hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'robot_hints', problemDef, commonDef), + commError = function (error) { + alert(error); + }; + + codeq.tr.registerDictionary('robot', problemDef.translations); + codeq.tr.translateDom(jqScreen); + if (currentSolution) editor.setValue(currentSolution); + jqBtnPlan.prop('disabled', !hinter.hasNextPlan()); + + editor.on('change', function (instance, changeObj) { + var doc = editor.getDoc(), + pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)); + + if (changeObj.removed) { + activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed.join(''))}); + } + + if (changeObj.text) { + activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text}); + } + }); + + jqBtnPlan.on('click', function () { + if (!hinter.planNext()) { + jqBtnPlan.prop('disabled', true).blur(); + } + }); + jqBtnHint.on('click', function () { + var doc = editor.getDoc(); + codeq.comms.sendHint({ + 'language': 'robot', + 'program': editor.getDoc().getValue(), + 'problem_id': problemDef.id + }) + .then(function (data) { + if (data.code === 0) { + hinter.handle(data.hints); + } + else { + terminal.append('error: ' + data.message); + } + }) + .fail(commError) + .done(); + }); + jqBtnRun.on('click', function () { + var url = 'http://' + $('#robot_ip').val() + ':8000/run', + doc = editor.getDoc(); + + $.ajax(url, { + method: 'POST', + data: JSON.stringify({program: doc.getValue()}) + }).done(function (data) { + if (data.code === 0) { + terminal.append('<run>\n', 'output'); + } + else { + terminal.append('error: ' + data.message + '\n'); + } + }).fail(function (jqXHR, textStatus, err) { + commError('Could not access '+url); + }); + }); + jqBtnStop.on('click', function () { + var url = 'http://' + $('#robot_ip').val() + ':8000/stop'; + + $.ajax(url, { + method: 'POST' + }).done(function (data) { + if (data.code === 0) { + terminal.append('<stop>\n', 'output'); + } + else { + terminal.append('error: ' + data.message + '\n'); + } + }).fail(function (jqXHR, textStatus, err) { + commError('Could not access '+url); + }); + }); + + return { + destroy: function () { + $('#screen_robot .title').text('');//empty the title text + jqAllButtons.off(); + editor.off('change'); + codeq.comms.off('terminal_output'); // stop listening for the terminal events from server + hinter.destroy(); + terminal.destroy(); + jqDescriptionContent.empty(); + jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it + jqTerminal.empty(); // TODO: the same with the console + jqDescriptionContent = null; + jqEditor = null; + jqTerminal = null; + jqHints = null; + codeq.tr.registerDictionary('robot', codeq.tr.emptyDictionary); + } + }; + }; +})(); diff --git a/js/codeq/translation.js b/js/codeq/translation.js index d4278ac..67f49eb 100644 --- a/js/codeq/translation.js +++ b/js/codeq/translation.js @@ -2,7 +2,7 @@ var dicts = {}, translateElement = function (jqElt, lang) { - var dictionaryKey = jqElt.data('dict'), + var dictionaryKey = jqElt.data('dict') || 'gui', translationKey = jqElt.data('tkey'), dict = dicts[dictionaryKey], translations, html, key; @@ -43,7 +43,7 @@ jqElt.html(html); }, translateDocument = function (lang) { - $('.translatable').each(function () { + $('[data-tkey]').each(function () { translateElement($(this), lang); }); }; @@ -70,7 +70,10 @@ 'translateDom': function (jqTopElt) { var lang = codeq.getLang(); - jqTopElt.find('.translatable').each(function () { + jqTopElt.filter('[data-tkey]').each(function () { + translateElement(jqTopElt, lang) + }); + jqTopElt.find('[data-tkey]').each(function () { translateElement($(this), lang); }); } diff --git a/res/en.json b/res/en.json new file mode 100644 index 0000000..5330cdc --- /dev/null +++ b/res/en.json @@ -0,0 +1,15 @@ +{ + "language": "Language", + "problem": "Problem", + "python": "Python", + "prolog": "Prolog", + "robot": "Robot", + "logout": "Logout", + "profile": "Profile", + "change_pass": "Change password", + "btn_plan": "Plan", + "btn_hint": "Hint", + "btn_test": "Test", + "btn_run": "Run", + "btn_stop": "Stop" +}
\ No newline at end of file diff --git a/res/sl.json b/res/sl.json new file mode 100644 index 0000000..cdde4f4 --- /dev/null +++ b/res/sl.json @@ -0,0 +1,15 @@ +{ + "language": "Jezik", + "problem": "Problem", + "python": "Python", + "prolog": "Prolog", + "robot": "Robot", + "logout": "Odjava", + "profile": "Profil", + "change_pass": "Zamenjaj geslo", + "btn_plan": "Plan", + "btn_hint": "Namig", + "btn_test": "Testiraj", + "btn_run": "Zaženi", + "btn_stop": "Ustavi" +}
\ No newline at end of file |