/* CodeQ: an online programming tutor. Copyright (C) 2015 UL FRI This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ (function(){ "use strict"; 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 || [], html = [], problemReferences = [], groupReferences = [], groupProblemIDs, group, problems, problem, i, j, nrefs, baseTabIndex = 200; // tabindex attribute of the first problem link var langDict = convertTranslations(rawTranslations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict groupDict, problemDict; codeq.template.processDictionary(langDict.description, [languageIdentifier]); // title: HTML structure for "name" and "desc" html.push('


'); html.push('
'); // content: problem directory for (i = 0; i < groups.length; i++) { group = groups[i] || {}; problems = group.problems || []; groupDict = convertTranslations(group.translations, 'name', 'description'); // the group-level translations, added will codeq.template.processDictionary(groupDict.description, [languageIdentifier, group.identifier]); // group content html.push('

'); html.push('
'); html.push(''); } 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 elements 'groups': groupReferences, // array of group info [problem_id1, problem_id2, …] '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('.group-problems a').on('click', function (e) { var index = +$(this).attr('class').split(' ')[0].split('-')[1], ref = data.refs[index]; e.preventDefault(); if (!ref) { codeq.log.error('Clicked on a problem link having erroneous index: ' + index); return; } codeq.globalStateMachine.transition(language,ref,data); }); }, /** * Update mutable data in the problem list screen. */ updateDom = function (languageData, attempts) { var problems = languageData.refs, groups = languageData.groups, jqGroup = jqScreen.find('h2.group-title'), i, ref, jqLink; // show attempted/solved status in the link for each problem for (i = 0; i < languageData.refs.length; i++) { ref = languageData.refs[i]; jqLink = jqScreen.find('a.problem-'+i); jqLink.removeClass('attempted solved'); if (ref.id in attempts) { jqLink.addClass(attempts[ref.id] ? 'solved' : 'attempted'); } } // add links to view all solutions for each group jqGroup.find('a.view-solutions').remove(); jqGroup.each(function () { var group = +$(this).attr('class').split(' ')[1].split('-')[1], problem_ids = groups[group].filter(function (pid) { return pid in attempts }), link; if (problem_ids.length > 0) { var link = $('') .on('click', function (e) { e.preventDefault(); codeq.globalStateMachine.transition('solutions', problem_ids); }); $(this).append(' ', link); } }); }, // ================================================================================ // 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 'initial': rawData.initial, '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; }); }, loadProblemData = function (ref, data) { var language = data.language; return codeq.wait( Q.all([ codeq.comms.getCurrentSolution(ref.id), // the current user's solution to the selected problem 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: ' + userProblemData.message); } if (!generalProblemData) { throw new Error('General problem data is not defined.'); } return { 'generalProblemData': generalProblemData, 'solution': userProblemData.data.solution }; }) ); }, currentLanguage; // the currently active language codeq.loadProblemData = loadProblemData; // ================================================================================ // 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', { 'jqScreen': jqScreen, 'enter': function(language){ var data = languageCache[language]; // language data $("#navigation-problem-list").addClass("active").css('display', ''); if (!language) language = currentLanguage; // This happens when we hit this with the back button if (language !== currentLanguage) { jqScreen.empty(); } codeq.wait(Q.all([ data || codeq.comms.getLanguageDef(language), codeq.comms.getAttempts(language) ]).spread(function (langData, attemptData) { if (!data) { data = createLanguageData(langData, language); languageCache[language] = data; } if (language !== currentLanguage) { createDom(data); } currentLanguage = language; updateDom(data, attemptData.data); })).done(); jqScreen.css('display', ''); }, 'exit' : function(){ jqScreen.css('display', 'none'); $('#navigation-problem-list').css('display', 'none').removeClass("active"); } }); })();