summaryrefslogtreecommitdiff
path: root/js/codeq/problem_list.js
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@araneo.org>2015-10-06 18:32:42 +0200
committerTimotej Lazar <timotej.lazar@araneo.org>2015-10-06 18:32:42 +0200
commit0b2485f393bcc47dcf895044c410b2c8784af432 (patch)
treec47ce3db5e8ff35dccfe714b6c0635c01f07ee6d /js/codeq/problem_list.js
parent3f699e7460fb79bb0b48077dddc3359ee4b974c1 (diff)
Rename state 'problem' to problem_list
Remove the Language element from the navbar and have "CodeQ" (logo TBD) send user to the language-selection page.
Diffstat (limited to 'js/codeq/problem_list.js')
-rw-r--r--js/codeq/problem_list.js383
1 files changed, 383 insertions, 0 deletions
diff --git a/js/codeq/problem_list.js b/js/codeq/problem_list.js
new file mode 100644
index 0000000..1b20fe2
--- /dev/null
+++ b/js/codeq/problem_list.js
@@ -0,0 +1,383 @@
+(function(){
+ var jqScreen = $('#screen_problem_list'),
+ 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
+ // ================================================================================
+
+ defaultHintCondition = function (translation) {
+ // must contain at least one translation
+ var key;
+ if (!translation || typeof translation !== 'object') return false;
+ for (key in translation) {
+ if (!translation.hasOwnProperty(key)) continue;
+ return true;
+ }
+ return false;
+ },
+ 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', defaultHintCondition) || {};
+ 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 || !defaultHintCondition(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 = allHintKeys.length - 1; i >= 0; i--) {
+ allHints[allHintKeys[i]] = {}; // create keys with no translations, we'll fill them in the next loop
+ }
+ for (i = langs.length - 1; i >= 0; i--) {
+ lang = langs[i];
+ tr = rawTranslations[lang];
+ // set up hints
+ if (!tr || !defaultHintCondition(tr.hint)) {
+ // there's no hint in the current language, copy the default in its entirety
+ for (j = allHintKeys.length - 1; j >= 0; j--) {
+ key = allHintKeys[j];
+ allHints[key][lang] = defaultHint[key];
+ }
+ }
+ else {
+ // make a copy of all hints, using the default hint value where a hint value is missing
+ h = tr.hint;
+ for (j = allHintKeys.length - 1; j >= 0; j--) {
+ key = allHintKeys[j];
+ allHints[key][lang] = 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
+ // ================================================================================
+
+ defaultPlanCondition = function (translation) {
+ // default plan must be non-empty
+ 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) || [],
+ 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)) {
+ plan = tr.plan;
+ }
+ else {
+ // there's no plan in the current language, copy the default plan
+ 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];
+ }
+ }
+ // 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;
+ },
+
+ // ================================================================================
+ // Group + problems directory processing
+ // ================================================================================
+
+ chooseTranslation = function (keyword, lang, currentDict, enDict, translations) {
+ var tr = currentDict[keyword],
+ otherLang;
+ if (tr) return tr; // if there is a translation in the current dictionary: return it
+ tr = enDict[keyword];
+ if (tr) { // if there is a translation in the english dictionary: return it
+ codeq.log.info('Translation for ' + keyword + ' not set for language ' + lang + ', using translation from en');
+ return tr;
+ }
+ for (otherLang in translations) {
+ if (!translations.hasOwnProperty(otherLang)) continue;
+ tr = (translations[otherLang] || {})[keyword];
+ if (tr) { // otherwise: return the first available translation in any language
+ codeq.log.info('Translation for ' + keyword + ' not set for language ' + lang + ', using translation from ' + otherLang);
+ return tr;
+ }
+ }
+ return keyword + ' not set for language ' + lang;
+ },
+
+ /**
+ * convert the input translations (arg0) for given keys (arg1..argN)
+ * so each keys holds all its translations for every language
+ */
+ convertTranslations = function () {
+ var translations = arguments[0] || {},
+ result = {},
+ enDict = translations['en'] || {},
+ lang, dict, i, keyword, l;
+ // initialize result: one lang-dict per keyword
+ for (i = arguments.length - 1; i > 0; i--) result[arguments[i]] = {};
+ // convert translations: one keyword-dict per lang -> one lang-dict per keyword
+ for (l = Nlangs - 1; l >= 0; l--) {
+ lang = langs[l];
+ dict = translations[lang] || {};
+ for (i = arguments.length - 1; i > 0; i--) {
+ keyword = arguments[i];
+// result[keyword][lang] = dict[keyword] || keyword + ' not set for language ' + lang;
+ result[keyword][lang] = chooseTranslation(keyword, lang, dict, enDict, translations);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Connect the given translations of a key with the DOM, returning
+ * the string of arguments to use with a HTML tag which will contain
+ * a translation chosen from the given dictionary.
+ */
+ ta = function (trObj) { // an object of the form: {'en': 'english content', 'sl': 'slovenska vsebina'}
+ var result = ['data-dict="directory" data-tkey="', translationCache.length, '"'].join('');
+ translationCache.push(trObj);
+ return result;
+ },
+
+ /**
+ * Assemble the display structure and translations from the server's
+ * language.json for the given language identifier.
+ */
+ createLanguageData = function (data, languageIdentifier) { // data is the content of language.json
+ var li = languageIdentifier, // a shorthand
+ rawTranslations = data.translations || {},
+ groups = data.groups || [],
+ Ngroups = groups.length,
+ html = [],
+ problemReferences = [],
+ group, problems, Nproblems, problem, i, j;
+ 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" ', ta(langDict.name), '></h1><hr>');
+ html.push('<div class="language-description" ', ta(langDict.description), '></div>');
+ // content: problem directory
+ html.push('<ul class="language-problems">');
+ for (i = 0; i < Ngroups; i++) {
+ group = groups[i] || {};
+ groupDict = convertTranslations(group.translations, 'name', 'description'); // the group-level translations, added will
+ // group content
+ html.push('<li><div class="group-title" ', ta(groupDict.name), '></div>');
+ html.push('<div class="group-description" ', ta(groupDict.description), '></div>');
+ html.push('<ul class="group-problems">');
+ // problem content
+ problems = group.problems || [];
+ Nproblems = problems.length;
+ for (j = 0; j < Nproblems; j++) {
+ problem = problems[j] || {};
+ problemDict = convertTranslations(problem.translations, 'name');
+ html.push('<li><a class="problem-', '' + problemReferences.length, '" ', ta(problemDict.name), '></a></li>');
+ problemReferences.push({'g': group.identifier || 'nogroup', 'p': problem.identifier || 'noproblem', 'id': problem.id});
+ }
+ html.push('</ul></li>');
+ }
+ html.push('</ul>');
+
+ return {
+ '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
+ 'commonDef': {
+ 'hint': processHints(rawTranslations), // hint translations: keyword -> lang -> value
+ 'hint_type': data.hint_type || {}
+ }
+ };
+ },
+
+ // ================================================================================
+ // 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) { // data is the (cached) result of createLanguageData()
+ var language = data.language;
+ jqScreen.html(data.html);
+ codeq.tr.translateDom(jqScreen);
+ jqScreen.find('a').on('click', function () {
+ var index = +$(this).attr('class').split(' ')[0].split('-')[1],
+ ref = data.refs[index];
+ if (!ref) {
+ codeq.log.error('Clicked on a problem link having erroneous index: ' + index);
+ return;
+ }
+ codeq.wait(
+ 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.commonDef, userProblemData.data.solution);
+ })
+ )
+ .fail(function (reason) {
+ codeq.log.error('Failed to obtain the problem definition: ' + reason, reason);
+ alert('Failed to obtain the problem definition: ' + reason);
+ })
+ .done();
+ });
+ },
+
+ // ================================================================================
+ // Problem definition processing
+ // ================================================================================
+
+ chooseDefaultTranslation = function (rawTranslations, translationKey, condition) {
+ var tr = rawTranslations.en, // try English as the default
+ lang;
+ if (typeof condition !== 'function') condition = function (x) { return !!x; };
+ if (tr && condition(tr[translationKey])) return tr[translationKey];
+ for (lang in rawTranslations) { // find a translation with content
+ if (!rawTranslations.hasOwnProperty(lang)) continue;
+ tr = rawTranslations[lang];
+ if (tr && condition(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
+ 'hint_type': rawData.hint_type || {}
+ };
+ },
+
+ 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, language, group, problem);
+ groupCache[problem] = data;
+ return data;
+ });
+ },
+
+ currentLanguage; // the currently active language
+
+ // ================================================================================
+ // Initialization, invoked from the boot sequence
+ // ================================================================================
+
+ codeq.on('init', function () {
+ codeq.tr.registerDictionary('directory', translationCache);
+ langs = codeq.availableLangs; // cache for easier access
+ Nlangs = langs.length;
+ });
+
+ // ================================================================================
+ // Register with the state machine
+ // ================================================================================
+
+ codeq.globalStateMachine.register('problem_list', {
+ 'enter': function(language){
+ var data = null; // language data
+
+ $("#navigation-problem_list").addClass("active").css('display', '');
+
+ if (!language) language = currentLanguage; // This happens when we hit this with the back button
+
+ if (currentLanguage !== language) {
+ jqScreen.empty();
+ currentLanguage = language;
+ data = languageCache[language];
+ if (data) {
+ createDom(data);
+ }
+ else {
+ codeq.wait(codeq.comms.getLanguageDef(language).then(function (rawData) {
+ var data = createLanguageData(rawData, language);
+ languageCache[language] = data;
+ createDom(data);
+ })).done();
+ }
+ }
+
+ jqScreen.css('display', '');
+ },
+ 'exit' : function(){
+ jqScreen.css('display', 'none');
+ $('#navigation-problem_list').css('display', 'none').removeClass("active");
+ }
+ });
+})();