/* 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('');
// problem content
groupProblemIDs = [];
for (j = 0; j < problems.length; j++) {
nrefs = problemReferences.length;
problem = problems[j] || {};
problemDict = convertTranslations(problem.translations, 'name');
html.push(' ');
problemReferences.push({'g': group.identifier || 'nogroup', 'p': problem.identifier || 'noproblem', 'id': problem.id});
groupProblemIDs.push(problem.id);
}
groupReferences.push(groupProblemIDs);
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
'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");
}
});
})();