/* Programming contest management system * Copyright © 2012 Luca Wehrstedt * * 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 round(value, ndigits) { value *= Math.pow(10, ndigits); value = Math.round(value); value /= Math.pow(10, ndigits); return value; } function round_to_str(value, ndigits) { if (ndigits > 0) { // Remove trailing zeroes return value.toFixed(ndigits).replace(/\.?0+$/, ""); } else { return value.toFixed(0); } } var DataStore = new function () { var self = this; self.contests = new Object(); self.tasks = new Object(); self.teams = new Object(); self.users = new Object(); self.contest_create = $.Callbacks(); self.contest_update = $.Callbacks(); self.contest_delete = $.Callbacks(); self.task_create = $.Callbacks(); self.task_update = $.Callbacks(); self.task_delete = $.Callbacks(); self.team_create = $.Callbacks(); self.team_update = $.Callbacks(); self.team_delete = $.Callbacks(); self.user_create = $.Callbacks(); self.user_update = $.Callbacks(); self.user_delete = $.Callbacks(); self.score_events = $.Callbacks(); self.rank_events = $.Callbacks(); ////// Contest self.contest_count = 0; self.init_contests = function () { $.ajax({ url: Config.get_contest_list_url(), dataType: "json", success: function (data, status, xhr) { self.contest_init_time = parseFloat(xhr.getResponseHeader("Timestamp")); for (var key in data) { self.create_contest(key, data[key]); } self.init_tasks(); }, error: function () { console.error("Error while getting the list of contests"); self.update_network_status(4); } }); } self.contest_listener = function (event) { var cmd = event.data.split(" "); if (cmd[0] == "create") { $.ajax({ url: Config.get_contest_read_url(cmd[1]), dataType: "json", success: function (data) { self.create_contest(cmd[1], data); }, error: function () { console.error("Error while getting contest " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "update") { $.ajax({ url: Config.get_contest_read_url(cmd[1]), dataType: "json", success: function (data) { self.update_contest(cmd[1], data); }, error: function () { console.error("Error while getting contest " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "delete") { self.delete_contest(cmd[1]); } }; self.create_contest = function (key, data) { data["key"] = key; self.contests[key] = data; console.info("Created contest " + key); console.log(data); self.contest_count += 1; self.contest_create.fire(key, data); }; self.update_contest = function (key, data) { var old_data = self.contests[key]; data["key"] = key; self.contests[key] = data; console.info("Updated contest " + key); console.log(old_data); console.log(data); self.contest_update.fire(key, old_data, data); }; self.delete_contest = function (key) { var old_data = self.contests[key]; delete self.contests[key]; console.info("Deleted contest " + key); console.log(old_data); self.contest_count -= 1; self.contest_delete.fire(key, old_data); }; ////// Task self.task_count = 0; self.init_tasks = function () { $.ajax({ url: Config.get_task_list_url(), dataType: "json", success: function (data, status, xhr) { self.task_init_time = parseFloat(xhr.getResponseHeader("Timestamp")); for (var key in data) { self.create_task(key, data[key]); } self.inits_todo -= 1; if (self.inits_todo == 0) { self.init_scores(); } }, error: function () { console.error("Error while getting the list of tasks"); self.update_network_status(4); } }); } self.task_listener = function (event) { var cmd = event.data.split(" "); if (cmd[0] == "create") { $.ajax({ url: Config.get_task_read_url(cmd[1]), dataType: "json", success: function (data) { self.create_task(cmd[1], data); }, error: function () { console.error("Error while getting task " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "update") { $.ajax({ url: Config.get_task_read_url(cmd[1]), dataType: "json", success: function (data) { self.update_task(cmd[1], data); }, error: function () { console.error("Error while getting task " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "delete") { self.delete_task(cmd[1]); } }; self.create_task = function (key, data) { if (self.contests[data["contest"]] === undefined) { console.error("Could not find contest " + data["contest"] + " for task " + key); if (self.es) { self.es.close(); } self.update_network_status(4); return; } data["key"] = key; self.tasks[key] = data; console.info("Created task " + key); console.log(data); self.task_count += 1; self.task_create.fire(key, data); }; self.update_task = function (key, data) { var old_data = self.tasks[key]; data["key"] = key; self.tasks[key] = data; console.info("Updated task " + key); console.log(old_data); console.log(data); self.task_update.fire(key, old_data, data); }; self.delete_task = function (key) { var old_data = self.tasks[key]; delete self.tasks[key]; console.info("Deleted task " + key); console.log(old_data); self.task_count -= 1; self.task_delete.fire(key, old_data); }; ////// Team self.team_count = 0; self.init_teams = function () { $.ajax({ url: Config.get_team_list_url(), dataType: "json", success: function (data, status, xhr) { self.team_init_time = parseFloat(xhr.getResponseHeader("Timestamp")); for (var key in data) { self.create_team(key, data[key]); } self.init_users(); }, error: function () { console.error("Error while getting the list of teams"); self.update_network_status(4); } }); } self.team_listener = function (event) { var cmd = event.data.split(" "); if (cmd[0] == "create") { $.ajax({ url: Config.get_team_read_url(cmd[1]), dataType: "json", success: function (data) { self.create_team(cmd[1], data); }, error: function () { console.error("Error while getting team " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "update") { $.ajax({ url: Config.get_team_read_url(cmd[1]), dataType: "json", success: function (data) { self.update_team(cmd[1], data); }, error: function () { console.error("Error while getting team " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "delete") { self.delete_team(cmd[1]); } }; self.create_team = function (key, data) { data["key"] = key; self.teams[key] = data; console.info("Created team " + key); console.log(data); self.team_count += 1; self.team_create.fire(key, data); }; self.update_team = function (key, data) { var old_data = self.teams[key]; data["key"] = key; self.teams[key] = data; console.info("Updated team " + key); console.log(old_data); console.log(data); self.team_update.fire(key, old_data, data); }; self.delete_team = function (key) { var old_data = self.teams[key]; delete self.teams[key]; console.info("Deleted team " + key); console.log(old_data); self.team_count -= 1; self.team_delete.fire(key, old_data); }; ////// User self.user_count = 0; self.init_users = function () { $.ajax({ url: Config.get_user_list_url(), dataType: "json", success: function (data, status, xhr) { self.user_init_time = parseFloat(xhr.getResponseHeader("Timestamp")); for (var key in data) { self.create_user(key, data[key]); } self.inits_todo -= 1; if (self.inits_todo == 0) { self.init_scores(); } }, error: function () { console.error("Error while getting the list of users"); self.update_network_status(4); } }); } self.user_listener = function (event) { var cmd = event.data.split(" "); if (cmd[0] == "create") { $.ajax({ url: Config.get_user_read_url(cmd[1]), dataType: "json", success: function (data) { self.create_user(cmd[1], data); }, error: function () { console.error("Error while getting user " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "update") { $.ajax({ url: Config.get_user_read_url(cmd[1]), dataType: "json", success: function (data) { self.update_user(cmd[1], data); }, error: function () { console.error("Error while getting user " + cmd[1]); self.es.close(); self.update_network_status(4); } }); } else if (cmd[0] == "delete") { self.delete_user(cmd[1]); } }; self.create_user = function (key, data) { if (data["team"] !== null && self.teams[data["team"]] === undefined) { console.error("Could not find team " + data["team"] + " for user " + key); if (self.es) { self.es.close(); } self.update_network_status(4); return; } data["key"] = key; self.users[key] = data; console.info("Created user " + key); console.log(data); self.user_count += 1; self.user_create.fire(key, data); }; self.update_user = function (key, data) { var old_data = self.users[key]; data["key"] = key; self.users[key] = data; console.info("Updated user " + key); console.log(old_data); console.log(data); self.user_update.fire(key, old_data, data); }; self.delete_user = function (key) { var old_data = self.users[key]; delete self.users[key]; console.info("Deleted user " + key); console.log(old_data); self.user_count -= 1; self.user_delete.fire(key, old_data); }; ////// Default scores self.global_max_score = 0.0; self.global_score_precision = 0; self.contest_create.add(function (key, data) { // Add scores for (var u_id in self.users) { self.users[u_id]["c_" + key] = 0.0; } // Maximum score data["max_score"] = 0.0; // Global score precision self.global_score_precision = 0; for (var c_id in self.contests) { self.global_score_precision = Math.max(self.global_score_precision, self.contests[c_id]["score_precision"]); } }); self.contest_update.add(function (key, old_data, data) { // Maximum score data["max_score"] = old_data["max_score"]; delete old_data["max_score"]; // Global score precision self.global_score_precision = 0; for (var c_id in self.contests) { self.global_score_precision = Math.max(self.global_score_precision, self.contests[c_id]["score_precision"]); } }); self.contest_delete.add(function (key, old_data) { // Remove scores for (var u_id in self.users) { delete self.users[u_id]["c_" + key]; } // Maximum score delete old_data["max_score"]; // Global score precision self.global_score_precision = 0; for (var c_id in self.contests) { self.global_score_precision = Math.max(self.global_score_precision, self.contests[c_id]["score_precision"]); } }); self.task_create.add(function (key, data) { // Add scores for (var u_id in self.users) { self.users[u_id]["t_" + key] = 0.0; } // Maximum score self.contests[data["contest"]]["max_score"] += data["max_score"]; self.global_max_score += data["max_score"]; }); self.task_update.add(function (key, old_data, data) { /* TODO: We may want to check that all scores are still less than or equal to the maximum achievable score. Or we may assume that this is handled by the server. */ // Maximum score self.contests[old_data["contest"]]["max_score"] -= old_data["max_score"]; self.global_max_score -= old_data["max_score"]; self.contests[data["contest"]]["max_score"] += data["max_score"]; self.global_max_score += data["max_score"]; }); self.task_delete.add(function (key, old_data) { // Remove scores for (var u_id in self.users) { delete self.users[u_id]["t_" + key]; } // Maximum score self.contests[old_data["contest"]]["max_score"] -= old_data["max_score"]; self.global_max_score -= old_data["max_score"]; }); self.user_create.add(function (key, data) { // Add scores for (var t_id in self.tasks) { data["t_" + t_id] = 0.0; } for (var c_id in self.contests) { data["c_" + c_id] = 0.0; } data["global"] = 0.0; }); self.user_update.add(function (key, old_data, data) { // Copy scores for (var t_id in self.tasks) { data["t_" + t_id] = old_data["t_" + t_id]; delete old_data["t_" + t_id]; } for (var c_id in self.contests) { data["c_" + c_id] = old_data["c_" + c_id]; delete old_data["c_" + c_id]; } data["global"] = old_data["global"]; delete old_data["global"]; }); self.user_delete.add(function (key, old_data) { // Remove scores for (var t_id in self.tasks) { delete old_data["t_" + t_id]; } for (var c_id in self.contests) { delete old_data["c_" + c_id]; } delete old_data["global"]; }); ////// Score self.init_scores = function () { $.ajax({ url: Config.get_score_url(), dataType: "json", success: function (data, status, xhr) { self.score_init_time = parseFloat(xhr.getResponseHeader("Timestamp")); for (var u_id in data) { for (var t_id in data[u_id]) { self.set_score(u_id, t_id, data[u_id][t_id]); } } self.init_ranks(); }, error: function () { console.error("Error while getting the scores"); self.update_network_status(4); } }); }; self.score_listener = function (event) { var data = event.data.split("\n"); for (var idx in data) { var line = data[idx].split(" "); self.set_score(line[0], line[1], parseFloat(line[2])); } }; self.set_score = function (u_id, t_id, new_t_score) { /* It may be "nice" to check that the user and task do actually exists, even if the server should already ensure it! */ var user = self.users[u_id]; var task = self.tasks[t_id]; var c_id = task["contest"]; var contest = self.contests[c_id]; // Task new_t_score = round(new_t_score, task["score_precision"]); var old_t_score = user["t_" + t_id]; user["t_" + t_id] = new_t_score; // Contest var new_c_score = 0.0; // = max(user's score on t for t in contest.tasks) for (var i = 0; i < contest.tasks.length; i += 1) { new_c_score += user["t_" + contest.tasks[i].key]; } new_c_score = round(new_c_score, contest["score_precision"]); var old_c_score = user["c_" + c_id]; user["c_" + c_id] = new_c_score; // Global var new_g_score = 0.0; // = max(user's score on c for c in self.contest_list) for (var i = 0; i < self.contest_list.length; i += 1) { new_g_score += user["c_" + self.contest_list[i].key]; } new_g_score = round(new_g_score, self.global_score_precision); var old_g_score = user["global"]; user["global"] = new_g_score; console.info("Changed score for user " + u_id + " and task " + t_id + ": " + old_t_score + " -> " + new_t_score); self.score_events.fire(u_id, user, t_id, task, new_g_score - old_g_score); }; self.get_score_t = function (u_id, t_id) { return self.users[u_id]["t_" + t_id]; }; self.get_score_c = function (u_id, c_id) { return self.users[u_id]["c_" + c_id]; }; self.get_score = function (u_id) { return self.users[u_id]["global"]; }; ////// Rank self.init_ranks = function () { // Make a list of all users var list = new Array(); for (var u_id in self.users) { list.push(self.users[u_id]); } // Sort it by decreasing score list.sort(function (a, b) { return b["global"] - a["global"]; }); // Assign ranks var prev_score = null; var rank = 0; var equal = 1; for (var i in list) { user = list[i]; score = user["global"]; if (score === prev_score) { equal += 1; } else { prev_score = score; rank += equal; equal = 1; } user["rank"] = rank; } self.score_events.add(self.update_rank); self.user_create.add(function (u_id, user) { /* We're actually just counting how many users have a non-zero global score and setting the rank of the new user to that number plus one. An optimization could be to store that number and to keep it up-to-date (instead of computing it every time). But since user creation is a rare event we could keep it this way. */ var new_rank = 1; for (var u_id in self.users) { if (self.users[u_id]["global"] > user["global"]) { new_rank += 1; } } user["rank"] = new_rank; }); self.user_update.add(function (u_id, old_user, user) { user["rank"] = old_user["rank"]; delete old_user["rank"]; }); self.user_update.add(function (u_id, old_user) { /* We're assuming that the user has its score set to zero and thus has the highest possible rank (this is because the server should have already deleted all its submissions and subchanges). So its deletion won't cause changes in others' ranks. */ delete old_user["rank"]; }); self.create_event_source(); self.init_selections(); }; self.update_rank = function (u_id, user) { /* The rank of a user is defined as the number of other users that have a score greater than theirs, plus one. Note that: a.score < b.score if and only if a.rank > b.rank a.score = b.score if and only if a.rank = b.rank a.score > b.score if and only if a.rank < b.rank Thus it's the same to sort by ascending score or by descending rank, and vice versa. When a user's score is updated (e.g. it changes from old_score to new_score with new_score > old_score) it can be shown that: - all users whose score is >= new_score don't change their rank - all users whose score is < old_score don't change their rank - all other users have their rank increased by exactly one A similar proposition holds when new_score < old_score. */ // We don't know old_score but we'll see that it's not needed. var new_score = user["global"]; var old_rank = user["rank"]; // The new rank is computed by strictly applying the definition: // new_rank = 1 + |{user2 in users, user2.score > user.score}| var new_rank = 1; for (var u2_id in self.users) { var user2 = self.users[u2_id]; // this condition is equivalent to // old_score <= user2["global"] < new_score if (old_rank >= user2["rank"] && user2["global"] < new_score) { user2["rank"] += 1; self.rank_events.fire(u2_id, user2, +1); // this condition is equivalent to // new_score <= user2["global"] < old_score } else if (new_score <= user2["global"] && user2["rank"] > old_rank) { user2["rank"] -= 1; self.rank_events.fire(u2_id, user2, -1); } if (user2["global"] > new_score) { new_rank += 1; } } user["rank"] = new_rank; if (old_rank != new_rank) { console.info("Changed rank for user " + u_id + ": " + old_rank + " -> " + new_rank); self.rank_events.fire(u_id, user, new_rank - old_rank); } }; ////// Initialization /* The init process works this way: - we start init_contests() and init_teams() - they each start an asynchronous AJAX request - when the requests end their data is processed and then, respectively, init_tasks() and init_users() are called - they also start an AJAX request and process its data - when BOTH requests finish init_scores() is called - it does again an AJAX request and processes its data - at the end it calls init_ranks() which calls init_selections() which, in turn, calls init_callback() */ self.init = function (callback) { self.inits_todo = 2; self.init_callback = callback; self.init_contests(); self.init_teams(); }; ////// Event listeners /* We set the listeners for Server Sent Events. */ self.last_event_id = null; self.create_event_source = function () { if (self.last_event_id == null) { self.last_event_id = Math.round(Math.min(self.contest_init_time, self.task_init_time, self.team_init_time, self.user_init_time, self.score_init_time) * 1000000).toString(16); } if (self.es) { delete self.es; } self.es = new EventSource(Config.get_event_url(self.last_event_id)); self.es.addEventListener("open", self.es_open_handler, false); self.es.addEventListener("error", self.es_error_handler, false); self.es.addEventListener("reload", self.es_reload_handler, false); self.es.addEventListener("contest", function (event) { var timestamp = parseInt(event.lastEventId, 16) / 1000000; if (timestamp > self.contest_init_time) { self.contest_listener(event); } self.last_event_id = event.lastEventId; }, false); self.es.addEventListener("task", function (event) { var timestamp = parseInt(event.lastEventId, 16) / 1000000; if (timestamp > self.task_init_time) { self.task_listener(event); } self.last_event_id = event.lastEventId; }, false); self.es.addEventListener("team", function (event) { var timestamp = parseInt(event.lastEventId, 16) / 1000000; if (timestamp > self.team_init_time) { self.team_listener(event); } self.last_event_id = event.lastEventId; }, false); self.es.addEventListener("user", function (event) { var timestamp = parseInt(event.lastEventId, 16) / 1000000; if (timestamp > self.user_init_time) { self.user_listener(event); } self.last_event_id = event.lastEventId; }, false); self.es.addEventListener("score", function (event) { var timestamp = parseInt(event.lastEventId, 16) / 1000000; if (timestamp > self.score_init_time) { self.score_listener(event); } self.last_event_id = event.lastEventId; }, false); }; self.update_network_status = function (state) { if (state == 0) { // self.es.CONNECTING $("#ConnectionStatus_box").attr("data-status", "reconnecting"); $("#ConnectionStatus_text").text("You are disconnected from the server but your browser is trying to connect."); } else if (state == 1) { // self.es.OPEN $("#ConnectionStatus_box").attr("data-status", "connected"); $("#ConnectionStatus_text").text("You are connected to the server and are receiving live updates."); } else if (state == 2) { // self.es.CLOSED $("#ConnectionStatus_box").attr("data-status", "disconnected"); $("#ConnectionStatus_text").html("You are disconnected from the server but you can try to connect."); } else if (state == 3) { // "reload" event received $("#ConnectionStatus_box").attr("data-status", "outdated"); $("#ConnectionStatus_text").html("Your local data cannot be updated. Please reload the page."); } else if (state == 4) { // an init failed $("#ConnectionStatus_box").attr("data-status", "init_error"); $("#ConnectionStatus_text").html("An error occurred while loading the data. Check your connection and reload the page."); } }; self.es_open_handler = function () { if (self.es.readyState == self.es.OPEN) { console.info("EventSource connected"); self.update_network_status(self.es.readyState); } else { console.error("EventSource shouldn't be in state " + self.es.readyState + " during a 'open' event!"); } }; self.es_error_handler = function () { if (self.es.readyState == self.es.CONNECTING) { console.info("EventSource reconnecting"); self.update_network_status(self.es.readyState); } else if (self.es.readyState == self.es.CLOSED) { console.info("EventSource disconnected"); self.update_network_status(self.es.readyState); } else { console.error("EventSource shouldn't be in state " + self.es.readyState + " during a 'error' event!"); } }; self.es_reload_handler = function () { if (self.es.readyState == self.es.OPEN) { console.info("Received a 'reload' event"); self.es.close(); self.update_network_status(3); } else { console.error("EventSource shouldn't be in state " + self.es.readyState + " during a 'reload' event!"); } }; $(document).ready(function () { self.update_network_status(0); }); ////// Sorted contest list self.contest_list = new Array(); self.contest_list_insert = function (key, data) { // Insert data in the sorted contest list var a = data; for (var i = 0; i < self.contest_list.length; i += 1) { var b = self.contest_list[i]; if ((a["begin"] < b["begin"]) || ((a["begin"] == b["begin"]) && ((a["end"] < b["end"] ) || ((a["end"] == b["end"] ) && ((a["name"] < b["name"] ) || ((a["name"] == b["name"] ) && (key < b["key"]))))))) { // We found the first element which is greater than a self.contest_list.splice(i, 0, a); return; } } self.contest_list.push(a); }; self.contest_list_remove = function (key, old_data) { // Remove data from the sorted contest list for (var i = 0; i < self.contest_list.length; i += 1) { var b = self.contest_list[i]; if (key == b["key"]) { self.contest_list.splice(i, 1); return; } } }; self.contest_create.add(function (key, data) { data["tasks"] = new Array(); self.contest_list_insert(key, data); }); self.contest_update.add(function (key, old_data, data) { data["tasks"] = old_data["tasks"]; delete old_data["tasks"]; self.contest_list_remove(key, old_data); self.contest_list_insert(key, data); }); self.contest_delete.add(function (key, old_data) { delete old_data["tasks"]; self.contest_list_remove(key, old_data); }); ////// Sorted task list self.task_list_insert = function (key, data) { var task_list = self.contests[data["contest"]]["tasks"]; // Insert data in the sorted task list of the contest var a = data; for (var i = 0; i < task_list.length; i += 1) { var b = task_list[i]; if ((a["order"] < b["order"]) || ((a["order"] == b["order"]) && ((a["name"] < b["name"] ) || ((a["name"] == b["name"] ) && (key < b["key"]))))) { // We found the first element which is greater than a task_list.splice(i, 0, a); return; } } task_list.push(a); }; self.task_list_remove = function (key, old_data) { var task_list = self.contests[old_data["contest"]]["tasks"]; // Remove data from the sorted task list of the contest for (var i = 0; i < task_list.length; i += 1) { var b = task_list[i]; if (key == b["key"]) { task_list.splice(i, 1); break; } } }; self.task_create.add(self.task_list_insert); self.task_update.add(function (key, old_data, data) { self.task_list_remove(key, old_data); self.task_list_insert(key, data); }); self.task_delete.add(self.task_list_remove); ////// Sorted team list self.team_list = new Array(); self.team_list_insert = function (key, data) { // Insert data in the sorted team list var a = data; for (var i = 0; i < self.team_list.length; i += 1) { var b = self.team_list[i]; if ((a["name"] < b["name"]) || ((a["name"] == b["name"]) && (key < b["key"]))) { // We found the first element which is greater than a self.team_list.splice(i, 0, a); return; } } self.team_list.push(a); }; self.team_list_remove = function (key, old_data) { // Remove data from the sorted team list for (var i = 0; i < self.team_list.length; i += 1) { var b = self.team_list[i]; if (key == b["key"]) { self.team_list.splice(i, 1); break; } } } self.team_create.add(function (key, data) { data["users"] = new Array(); self.team_list_insert(key, data); }); self.team_update.add(function (key, old_data, data) { data["users"] = old_data["users"]; delete old_data["users"]; self.team_list_remove(key, old_data); self.team_list_insert(key, data); }); self.team_delete.add(function (key, old_data) { delete old_data["users"]; self.team_list_remove(key, old_data); }); ////// Sorted user list self.user_list_insert = function (key, data) { if (data["team"] == null) { return; } var user_list = self.teams[data["team"]]["users"]; // Insert data in the sorted user list of the team var a = data; for (var i = 0; i < user_list.length; i += 1) { var b = user_list[i]; if ((a["l_name"] < b["l_name"]) || ((a["l_name"] == b["l_name"]) && ((a["f_name"] < b["f_name"]) || ((a["f_name"] == b["f_name"]) && (key < b["key"]))))) { // We found the first element which is greater than a user_list.splice(i, 0, a); return; } } user_list.push(a); }; self.user_list_remove = function (key, old_data) { if (old_data["team"] == null) { return; } var user_list = self.teams[old_data["team"]]["users"]; // Remove data from the sorted user list of the team for (var i = 0; i < user_list.length; i += 1) { var b = user_list[i]; if (key == b["key"]) { user_list.splice(i, 1); break; } } }; self.user_create.add(self.user_list_insert); self.user_update.add(function (key, old_data, data) { self.user_list_remove(key, old_data); self.user_list_insert(key, data); }); self.user_delete.add(self.user_list_remove); ////// Selection self.select_events = $.Callbacks(); /* We use eight different colors. We keep track of how many times each color is in use and when we have to assign a new color to an user we choose the one that has been used less times. */ self.colors = [0,0,0,0,0,0,0,0] self.choose_color = function () { var min_idx = 0; for (var i = 1; i < 8; i += 1) { if (self.colors[i] < self.colors[min_idx]) { min_idx = i; } } // Color indexes will be 1-based, so we add 1 to the result return min_idx+1; } self.init_selections = function () { $.each(self.users, function (u_id) { var color_idx = parseInt(localStorage.getItem("cms.rws.selection.users." + u_id)); if (color_idx > 0) { self.set_selected(u_id, true, color_idx); } }); $(window).on("storage", function (event) { event = event.originalEvent; if (event.storageArea == localStorage) { if (event.key === null) { // Triggered by a .clear(). $.each(self.users, function (u_id) { self.set_selected(u_id, false); }); } else if (event.key.lastIndexOf("cms.rws.selection.users.", 0) === 0) { var u_id = event.key.substr(24); if (event.oldValue === null && event.newValue !== null) { self.set_selected(u_id, true, parseInt(event.newValue)); } else if (event.oldValue !== null && event.newValue === null) { self.set_selected(u_id, false); } } } }); self.init_callback(); }; self.set_selected = function (u_id, flag, color_idx) { if (self.users[u_id]["selected"] == 0 && flag) { // We have to assign a color if (!(color_idx > 0)) { color_idx = self.choose_color(); } self.users[u_id]["selected"] = color_idx; self.colors[color_idx-1] += 1; localStorage.setItem("cms.rws.selection.users." + u_id, color_idx); self.select_events.fire(u_id, color_idx); } else if (self.users[u_id]["selected"] != 0 && !flag) { // We have to remove the color var color_idx = self.users[u_id]["selected"]; self.users[u_id]["selected"] = 0; self.colors[color_idx-1] -= 1; localStorage.removeItem("cms.rws.selection.users." + u_id); self.select_events.fire(u_id, 0); } }; self.toggle_selected = function (u_id) { self.set_selected(u_id, self.users[u_id]["selected"] == 0); }; self.get_selected = function (u_id) { return self.users[u_id]["selected"]; }; self.user_create.add(function (key, data) { data["selected"] = 0; }); self.user_update.add(function (key, old_data, data) { data["selected"] = old_data["selected"]; delete old_data["selected"]; }); self.user_delete.add(function (key, old_data) { self.set_selected(key, false); delete old_data["selected"]; }); };