diff options
author | Aleš Smodiš <aless@guru.si> | 2015-09-28 18:44:48 +0200 |
---|---|---|
committer | Aleš Smodiš <aless@guru.si> | 2015-09-28 18:44:48 +0200 |
commit | 7dab4d3640b7b37c407eea111eda1fc0b71adbda (patch) | |
tree | f6d422f294d796706634de43826bce117fef409a /js/codeq | |
parent | 1e0c68482267f7e7393d2cde5042fcd863b8e23b (diff) |
Implement in-structure GUI translation for python and problem editing screens.
Hints are not yet covered.
Diffstat (limited to 'js/codeq')
-rw-r--r-- | js/codeq/comms.js | 8 | ||||
-rw-r--r-- | js/codeq/hint.js | 23 | ||||
-rw-r--r-- | js/codeq/navigation.js | 2 | ||||
-rw-r--r-- | js/codeq/problem.js | 173 | ||||
-rw-r--r-- | js/codeq/prolog.js | 33 | ||||
-rw-r--r-- | js/codeq/python.js | 33 | ||||
-rw-r--r-- | js/codeq/translation.js | 7 |
7 files changed, 236 insertions, 43 deletions
diff --git a/js/codeq/comms.js b/js/codeq/comms.js index 2d934bd..5ac3d66 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -164,8 +164,8 @@ // AJAX communication support functions (loading of static web resources) // ================================================================================ - var languageCache = {},// language defs, keyed by language identifier - problemCache = {},// problem cache, 3-level, keyed by: language, problem group, and problem identifier + 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({ @@ -407,6 +407,10 @@ ); languageCache[identifier] = x; return x; + }, + + 'getProblemDef': function (language, group, problem) { + return ajaxGet(ajaxPrefix + language + '/' + group + '/' + problem + '/problem.json'); } }; })(); diff --git a/js/codeq/hint.js b/js/codeq/hint.js index e1c7a6b..1b97afc 100644 --- a/js/codeq/hint.js +++ b/js/codeq/hint.js @@ -7,10 +7,11 @@ var firstCharacterPos = {'line': 0, 'ch': 0}, sel_no_scroll = {'scroll': false}; - codeq.makeHinter = function (jqHints, jqEditor, editor, hintDefs, planDef) { + codeq.makeHinter = function (jqHints, jqEditor, editor, trNamespace, hintDefsA, commonHintDefsA, planDef) { var hintCounter = 0, // for generating unique class-names hintCleaners = [], planIdx = 0, + dictionary = [], clearHints = function () { var i; @@ -130,6 +131,25 @@ } }; + codeq.tr.registerDictionary(trNamespace, dictionary); + + // TODO: below is a temporary code to bridge the old implementation with the new data format + if (planDef.sl) planDef = planDef.sl; + else planDef = planDef.en || []; + var hintDefs = {}, t1, t2, k; + if (hintDefsA.sl) t1 = hintDefsA.sl; + else t1 = hintDefsA.en || {}; + if (commonHintDefsA.sl) t2 = commonHintDefsA.sl; + else t2 = commonHintDefsA.en || {}; + for (k in t2) { + if (!t2.hasOwnProperty(k)) continue; + hintDefs[k] = t2[k]; + } + for (k in t1) { + if (!t1.hasOwnProperty(k)) continue; + hintDefs[k] = t1[k]; + } + return { /** Display the next "planning" hint and return whether there are * any more available. @@ -195,6 +215,7 @@ 'destroy': function () { clearHints(); + codeq.tr.unregisterDictionary(trNamespace); jqHints.empty(); jqHints = null; jqEditor = null; diff --git a/js/codeq/navigation.js b/js/codeq/navigation.js index 4445a3d..30503e0 100644 --- a/js/codeq/navigation.js +++ b/js/codeq/navigation.js @@ -57,7 +57,7 @@ History.pushState({'state': name, 'params': Array.prototype.slice.apply(arguments, [1])}, null, '?s=' + name); } catch (e) { - codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e); + codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e, e); } }, 'destroy': function () { diff --git a/js/codeq/problem.js b/js/codeq/problem.js index 3e218d8..35b45af 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -2,8 +2,93 @@ var jqScreen = $('#screen_problem'), languageCache = {}, // keyed by language identifier: processed data about languages translationCache = [], // keys are autogenerated in ta(), a value is a dictionary of translations of a translation key for every language + problemCache = {}, // problem data cache, 3-level, keyed by: language, problem group, and problem identifier langs, Nlangs, // constants, set on init + // ================================================================================ + // Hint processing: extract hints from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processHints = function (rawTranslations) { + var defaultHint = {}, // here we put all the hints with their default translations + allHints = {}, // the result + allHintKeys = [], + tr, key, i, lang, hint, h, j; + // find the default hint translations, they will form the basis of default hints + tr = chooseDefaultTranslation(rawTranslations, 'hint') || {}; + for (key in tr) { // copy the hints + if (!tr.hasOwnProperty(key)) continue; + defaultHint[key] = tr[key]; + allHintKeys.push(key); + } + // copy any hints not in the default hints to the default hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + if (!tr || !tr.hint) continue; // skip unavailable translations or translations with no hints + hint = tr.hint; + for (key in hint) { + if (!hint.hasOwnProperty(key) || !hint[key]) continue; + if (!key in defaultHint) { + defaultHint[key] = hint[key]; + allHintKeys.push(key); + } + } + } + // create all translations for hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up hints + if (!tr || !tr.hint) { + // there's no hint in the current language, copy the default in its entirety + allHints[lang] = defaultHint; + } + else { + // make a copy of all hints, using the default hint value where a hint value is missing + hint = {}; + allHints[lang] = hint; + h = tr.hint; + for (j = allHintKeys.length; j >= 0; j--) { + key = allHintKeys[j]; + hint[key] = h[key] || defaultHint[key]; + } + } + } + return allHints; + }, + + // ================================================================================ + // Plan processing: extract plans from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processPlans = function (rawTranslations) { + // find the default plan translation + var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan') || [], + allPlans = {}, // the result + i, lang, tr; + // create all translations for plan + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up plan + if (!tr || !tr.plan) { + // there's no plan in the current language, copy the default plan + allPlans[lang] = defaultPlan; + } + else { + allPlans[lang] = tr.plan; + } + } + return allPlans; + }, + + // ================================================================================ + // Group + problems directory processing + // ================================================================================ + chooseTranslation = function (keyword, lang, currentDict, enDict, translations) { var tr = currentDict[keyword], otherLang; @@ -65,11 +150,12 @@ */ createLanguageData = function (data, languageIdentifier) { // data is the content of language.json var li = languageIdentifier, // a shorthand + rawTranslations = data.translations || {}, groups = data.groups || {}, html = [], problemReferences = [], groupIdentifier, group, problems, problemIdentifier, problem; - var langDict = convertTranslations(data.translations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict + var langDict = convertTranslations(rawTranslations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict groupDict, problemDict; // title: HTML structure for "name" and "desc" html.push('<h1 class="language-title translatable" ', ta(langDict.name), '></h1><hr>'); @@ -91,24 +177,29 @@ problem = problems[problemIdentifier] || {}; problemDict = convertTranslations(problem.translations, 'name'); html.push('<li><a class="problem-', '' + problemReferences.length, ' translatable" ', ta(problemDict.name), '></a></li>'); - problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier}); + problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier, 'id': problem.id}); } html.push('</ul></li>'); } html.push('</ul>'); return { - 'language': languageIdentifier, - 'html': html.join(''), - 'refs': problemReferences, - 'hints': {} // TODO: prepare common hints for the language + 'language': languageIdentifier, // 'prolog', 'python', ... + 'html': html.join(''), // the DOM structure (without textual content), as HTML text + 'refs': problemReferences, // array of problem info {g: group, p: problem, id: problem_id}, referenced from DOM <a> elements + 'hints': processHints(rawTranslations) // hint translations: keyword -> lang -> value }; }, + // ================================================================================ + // DOM instantiation. + // The transition to problem solving takes place here, after the user clicks on a + // problem and all the required data is loaded. + // ================================================================================ /** * Instantiates the screen from the given processed data. */ - createDom = function (data) { + createDom = function (data) { // data is the (cached) result of createLanguageData() var language = data.language; jqScreen.html(data.html); codeq.tr.translateDom(jqScreen); @@ -119,12 +210,15 @@ codeq.log.error('Clicked on a problem link having erroneous index: ' + index); return; } - // transition codeq.wait( - codeq.comms.getProblem(language, ref.g, ref.p) - .then(function (data) { - if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message); - codeq.globalStateMachine.transition(language, data); + Q.all([ + codeq.comms.getProblem(language, ref.g, ref.p), // TODO: use ref.id instead // the current solution + getProblemData(language, ref.g, ref.p) // the (cached) result of processProblemData() + ]) + .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.hints, userProblemData.solution); }) ) .fail(function (reason) { @@ -134,6 +228,61 @@ .done(); }); }, + + // ================================================================================ + // Problem definition processing + // ================================================================================ + + chooseDefaultTranslation = function (rawTranslations, translationKey) { + var tr = rawTranslations.en, // try English as the default + lang; + if (tr && tr[translationKey]) return tr[translationKey]; + for (lang in rawTranslations) { // find a translation with hints + if (!rawTranslations.hasOwnProperty(lang) || rawTranslations[lang]) continue; + tr = rawTranslations[lang]; + if (tr[translationKey]) return tr[translationKey]; + } + return null; // default must be chosen by the caller + }, + + processProblemData = function (rawData, language, group, problem) { + var rawTranslations = rawData.translations || {}; + return { + 'language': language, + 'group': group, + 'problem': problem, + 'id': rawData.id, + 'translations': convertTranslations(rawTranslations, 'title', 'name', 'slug', 'description'), // GUI translations: keyword -> lang -> value + 'hint': processHints(rawTranslations), // hint translations: keyword -> lang -> value + 'plan': processPlans(rawTranslations) // plan translations: keyword -> lang -> value + }; + }, + + getProblemData = function (language, group, problem) { + var langCache = problemCache[language], + groupCache, cachedProblem, promise; + if (langCache) { + groupCache = langCache[group]; + if (!groupCache) { + groupCache = {}; + langCache[group] = groupCache; + } + } + else { + langCache = {}; + problemCache[language] = langCache; + groupCache = {}; + langCache[group] = groupCache; + } + cachedProblem = groupCache[problem]; + if (cachedProblem) return Q(cachedProblem); + return codeq.comms.getProblemDef(language, group, problem).then(function (rawData) { + var data = processProblemData(rawData); + groupCache[problem] = data; + return data; + }); + }, + currentLanguage; // the currently active language // ================================================================================ diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js index be6b0c8..cd3fcb4 100644 --- a/js/codeq/prolog.js +++ b/js/codeq/prolog.js @@ -66,7 +66,7 @@ };
var prologHandler; //created when we enter the prolog state and destroyed once we leave it
codeq.globalStateMachine.register('prolog', {
- 'enter': function (data) {
+ 'enter': function (problemDef, commonHints, currentSolution) {
$('#navigation-login').css('display', '');
$('#navigation-language').css('display', '');
$('#navigation-problem').css('display', '');
@@ -74,7 +74,7 @@ $('#navigation-prolog').css('display', '');
jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly
- prologHandler = createPrologHandler(data.data);
+ prologHandler = createPrologHandler(problemDef, commonHints, currentSolution);
subScreens = codeq.makeStateMachine(substates);
subScreens.transition(jqDescription.data(stateNameTag));
/* Q.delay(100).then(function(){
@@ -245,30 +245,36 @@ };
};
+ codeq.on('init', function (args) {
+ codeq.tr.registerDictionary('prolog', 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}}
*/
- createPrologHandler = function (info) {
- var problem = info.problem,
+ createPrologHandler = function (problemDef, commonHints, currentSolution) {
+ var //problem = info.problem,
jqDescriptionContent = jqDescription.find('.description'),
jqEditor = jqCode.find('.code_editor'),
jqTerminal = jqConsole.find('.console'),
jqHints = jqInfo.find('.hints'),
editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }),
- activityHandler = makeActivityHandler(editor, problem.id),
- terminal = makePrologTerminalHandler(jqTerminal, editor, problem.id, activityHandler),
- hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan),
+ activityHandler = makeActivityHandler(editor, problemDef.id),
+ terminal = makePrologTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler),
+ hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'prolog_hints', problemDef.hint, commonHints, problemDef.plan),
commError = function (error) {
alert(error);
};
- editor.setValue(info.solution);
- $('#screen_prolog .title').text(problem.slug);
- jqDescriptionContent.html(problem.description);
- jqBtnPlan.prop('disabled', (problem.plan || '').length == 0);
+ codeq.tr.registerDictionary('prolog', problemDef.translations);
+ codeq.tr.translateDom(jqScreen);
+ if (currentSolution) editor.setValue(currentSolution);
+// $('#screen_prolog .title').text(problem.slug);
+// jqDescriptionContent.html(problem.description);
+ jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.en) || []).length == 0);
editor.on('change', function (instance, changeObj) {
var doc = editor.getDoc(),
@@ -295,7 +301,7 @@ codeq.comms.sendHint({
'language': 'prolog',
'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
+ 'problem_id': problemDef.id
})
.then(function (data) {
if (data.code === 0) {
@@ -319,7 +325,7 @@ codeq.comms.sendTest({
'language': 'prolog',
'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
+ 'problem_id': problemDef.id
})
.then(function (data) {
if (data.code === 0) {
@@ -351,6 +357,7 @@ jqEditor = null;
jqTerminal = null;
jqHints = null;
+ codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary);
}
};
};
diff --git a/js/codeq/python.js b/js/codeq/python.js index e80bbb7..8b66881 100644 --- a/js/codeq/python.js +++ b/js/codeq/python.js @@ -71,7 +71,7 @@ };
var pythonHandler; //created when we enter the python state and destroyed once we leave it
codeq.globalStateMachine.register('python', {
- 'enter': function (data) {
+ 'enter': function (problemDef, commonHints, currentSolution) {
$('#navigation-login').css('display', '');
$('#navigation-language').css('display', '');
$('#navigation-problem').css('display', '');
@@ -79,7 +79,7 @@ $('#navigation-python').css('display', '');
jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly
- pythonHandler = createPythonHandler(data.data);
+ pythonHandler = createPythonHandler(problemDef, commonHints, currentSolution);
subScreens = codeq.makeStateMachine(substates);
subScreens.transition(jqDescription.data(stateNameTag));
/* Q.delay(100).then(function(){
@@ -203,7 +203,9 @@ };
};
-
+ codeq.on('init', function (args) {
+ codeq.tr.registerDictionary('python', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active
+ });
/**
* Creates a new handler for the given Prolog assignment definition.
@@ -211,24 +213,26 @@ * @param {PrologTaskDef} info
* @returns {{destroy: Function, processServerHints: Function}}
*/
- var createPythonHandler = function (info) {
- var problem = info.problem,
+ var createPythonHandler = function (problemDef, commonHints, currentSolution) {
+ var //problem = info.problem,
jqDescriptionContent = jqDescription.find('.description'),
jqEditor = jqCode.find('.code_editor'),
jqTerminal = jqConsole.find('.console'),
jqHints = jqInfo.find('.hints'),
editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true, mode: 'python' }),
- activityHandler = makeActivityHandler(editor, problem.id),
- terminal = makePythonTerminalHandler(jqTerminal, editor, problem.id, activityHandler),
- hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan),
+ activityHandler = makeActivityHandler(editor, problemDef.id),
+ terminal = makePythonTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler),
+ hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'python_hints', problemDef.hint, commonHints, problemDef.plan),
commError = function (error) {
alert(error);
};
- editor.setValue(info.solution);
- $('#screen_python .title').text(problem.slug);
- jqDescriptionContent.html(problem.description);
- jqBtnPlan.prop('disabled', (problem.plan || '').length == 0);
+ codeq.tr.registerDictionary('python', problemDef.translations);
+ codeq.tr.translateDom(jqScreen);
+ if (currentSolution) editor.setValue(currentSolution);
+// $('#screen_python .title').text(problem.slug);
+// jqDescriptionContent.html(problem.description);
+ jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.en) || []).length == 0);
editor.on('change', function (instance, changeObj) {
var doc = editor.getDoc(),
@@ -253,7 +257,7 @@ codeq.comms.sendHint({
'language': 'python',
'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
+ 'problem_id': problemDef.id
})
.then(function (data) {
if (data.code === 0) {
@@ -271,7 +275,7 @@ codeq.comms.sendTest({
'language': 'python',
'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
+ 'problem_id': problemDef.id
})
.then(function (data) {
if (data.code === 0) {
@@ -323,6 +327,7 @@ jqEditor = null;
jqTerminal = null;
jqHints = null;
+ codeq.tr.registerDictionary('python', codeq.tr.emptyDictionary);
}
};
};
diff --git a/js/codeq/translation.js b/js/codeq/translation.js index af39cdc..d4278ac 100644 --- a/js/codeq/translation.js +++ b/js/codeq/translation.js @@ -6,6 +6,7 @@ translationKey = jqElt.data('tkey'), dict = dicts[dictionaryKey], translations, html, key; + if (dict === codeq.tr.emptyDictionary) return; // silent ignore if (!dict) { codeq.log.error('Cannot find translation dictionary ' + dictionaryKey); return; @@ -61,6 +62,12 @@ dicts[name] = dict; }, + 'unregisterDictionary': function (name) { + delete dicts[name]; + }, + + 'emptyDictionary': {}, // use this with registerDictionary when you don't want any translations + 'translateDom': function (jqTopElt) { var lang = codeq.getLang(); jqTopElt.find('.translatable').each(function () { |