summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authorAleš Smodiš <aless@guru.si>2015-09-28 18:44:48 +0200
committerAleš Smodiš <aless@guru.si>2015-09-28 18:44:48 +0200
commit7dab4d3640b7b37c407eea111eda1fc0b71adbda (patch)
treef6d422f294d796706634de43826bce117fef409a /js
parent1e0c68482267f7e7393d2cde5042fcd863b8e23b (diff)
Implement in-structure GUI translation for python and problem editing screens.
Hints are not yet covered.
Diffstat (limited to 'js')
-rw-r--r--js/codeq/comms.js8
-rw-r--r--js/codeq/hint.js23
-rw-r--r--js/codeq/navigation.js2
-rw-r--r--js/codeq/problem.js173
-rw-r--r--js/codeq/prolog.js33
-rw-r--r--js/codeq/python.js33
-rw-r--r--js/codeq/translation.js7
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 () {