Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
24 changes: 24 additions & 0 deletions handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
const scores = require("./lib/scores")
const activities = require("./lib/activities")
const status = require("./lib/status")
const submissions = require("./lib/submissions");
const problems = require("./lib/problems");

function db_client() {
let pg = require('pg');
Expand Down Expand Up @@ -51,3 +53,25 @@ completed for the first time since the last time it was checked.
module.exports.activities_compute = (event, context, callback) => {
return activities.compute(db_client(), callback);
};

/**
* Gather all the submissions of a given user
*/
module.exports.submissions_user = (event, context, callback) => {
return submissions.user(db_client(), event, callback);
};

/**
* Gather all the problems
*/
module.exports.problems_all = (event, context, callback) => {
return problems.all(db_client(), callback);
};

/**
* Update UVa problems
*/
module.exports.uva_update_problems = (event, context, callback) => {
require('./lib/platforms/uva.js').update_problems(db_client());
callback();
};
174 changes: 174 additions & 0 deletions lib/platforms/uva.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//=================
//==== UVA API ====
//=================

/**
* This file includes all the logic behind the UVa/uHunt part of the API.
*/

'use strict';

const request = require('request');
const sprintf = require('sprintf-js').sprintf;
const _ = require('lodash');

const UVA_USERNAME_TO_ID = 'https://uhunt.onlinejudge.org/api/uname2uid/%s';
const UVA_USER_SUBMISSIONS = 'http://uhunt.onlinejudge.org/api/subs-user/%s';
const UVA_PROBLEMS = 'http://uhunt.onlinejudge.org/api/p';

/**
* Make sure the user <user> has the correct user id in the database.
*/
function update_user_id(db, uva_username, callback) {
// uHunt query
request(sprintf(UVA_USERNAME_TO_ID, uva_username), (err, res, body) => {
if(err) throw err;

// Check for user in the db
db.query('SELECT * FROM uva_users WHERE uva_username = $1 LIMIT 1;', [uva_username], (err, result) => {
if(err) throw err;
var user = result.rows[0];
// If the user is not in db, add it
if(!user) {
// Don't add non existing users
if(body == 0)
return;

db.query('INSERT INTO uva_users (uva_id, uva_username) VALUES ($1::integer, $2) RETURNING *;', [body, uva_username], (err, result) => {
if(err) throw err;
callback(db, result.rows);
});
}
// If the user changed name, remove the submissions and update the user in the db
else if(body != user.uva_id) {
db.query("BEGIN;");
db.query("DELETE FROM submissions WHERE platform = 'UVa' AND username = $1;", [user.uva_username], (err) => {
if(err) throw err;
});
// Update if the user still has a valid ID
if(body != 0)
db.query("UPDATE uva_users SET uva_id = $1::integer WHERE uva_username = $2;", [body, user.uva_username], (err) => {
if(err) throw err;
});
// Remove if the user doesn't exist anymore
else
db.query("DELETE FROM uva_users WHERE uva_username = $1;", [user.uva_username], (err) => {
if(err) throw err;
});

db.query("COMMIT;", (err) => {
if(err) throw err;

// Only fetch the submissions if the user actually exists
if(body != 0)
callback(db, body);
});
}
else
callback(db, user);
});
});
}

/**
* Update the submissions for user <user>
*/
function update_user_submissions(db, user) {
request(sprintf(UVA_USER_SUBMISSIONS, user.uva_id), (err, res, body) => {
if(err) throw err;

/*
* Possible improvement: only ask for new submissions (with id > than the last one in the db)
* Drawbacks: more complicated and may cause missing data issues
*
* Also, doing all the updates in one query might actually be better.
*/

var data = JSON.parse(body).subs;
_.forEach(data, (sub) => {
db.query(
// SQL Query
'INSERT INTO submissions VALUES\n' +
'(\'UVa\', $1::integer, $2, $3, $4, $5, $6::integer, $7::bigint)\n' +
'ON CONFLICT(platform, platform_submission_id) DO UPDATE SET\n' +
'verdict = EXCLUDED.verdict,\n' +
'runtime = EXCLUDED.runtime;\n',
// Parameters
[sub[0], sub[1], user.uva_username, get_verdict_text(sub[2]), get_language_text(sub[5]),sub[3], sub[4]],
// Callback
(err) => {
if(err) throw err;
});
});
});
}

/**
* Get the submissions for user with username <uva_username>
*/
function get_user_submissions(db, uva_username) {
return new Promise((resolve, reject) => {
db.query('SELECT * FROM submissions WHERE platform = \'UVa\' AND username = $1;', [uva_username], (err, result) => {
if(err) throw err; // Should this reject ?

resolve(result.rows);
update_user_id(db, uva_username, update_user_submissions);
});
});
}

/**
* Update the stored problems
*/
function update_problems(db) {
request(UVA_PROBLEMS, (err, res, body) => {
if(err) throw err;

var data = JSON.parse(body);
_.forEach(data, (p) => {
db.query('INSERT INTO problems VALUES (\'UVa\', $1, $2, $3);', [p[0], p[1], p[2]], (err) => {
if(err) throw err;
});
});
});
}

module.exports = {
get_user_submissions: get_user_submissions,
update_problems: update_problems,
};

//=============================
//==== UTILITARY FUNCTIONS ====
//=============================
var verdicts = {
10: 'Submission error',
15: 'Can\'t be judged',
20: 'In judge queue',
30: 'Compile error',
35: 'Restricted function',
40: 'Runtime error',
45: 'Output limit exceeded',
50: 'Time limit exceeded',
60: 'Memory limit exceeded',
70: 'Wrong answer',
80: 'Presentation error',
90: 'Accepted',
};

function get_verdict_text(verdict) {
return verdicts[verdict] || 'In judge queue';
}

var languages = {
1: 'C',
2: 'Java',
3: 'C++',
4: 'Pascal',
5: 'C++', // C++11
6: 'Python', // Python 3
};

function get_language_text(language) {
return languages[language];
}
20 changes: 20 additions & 0 deletions lib/problems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var _ = require('lodash');

function all(db, callback) {
db.query('SELECT * FROM problems;', (err, result) => {
if(err) throw err;

const res = {
statusCode: 200,
body: {
problems: _.map(result.rows, p => [p.platform, p.platform_problem_id, p.platform_extra_data, p.title]),
},
};
callback(null, res);
});
}


module.exports = {
all,
};
40 changes: 40 additions & 0 deletions lib/submissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

var _ = require('lodash');
var uva_submissions = require('./platforms/uva').get_user_submissions;

function user(db, event, callback) {
var ret = [];

var user = {};

// Submission requests for all supported platforms
var arr = [];

// Allow custom id for better user identification
if(event.id) {
user.id = event.id;
}
// UVa
if(event.uva) {
user.uva = event.uva;
arr.push(uva_submissions(db, event.uva));
}

// Return them back to the client
Promise.all(arr).then(subs => {
const res = {
statusCode: 200,
body: {
user: user,
submissions: _.flatten(subs).map(s => [s.platform, s.platform_submission_id, s.platform_problem_id, s.username, s.verdict, s.language, s.runtime, s.time]),
},
};
callback(null, res);
});
}


module.exports = {
user,
};
65 changes: 65 additions & 0 deletions migrations/20170520181147-uva-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

var dbm;
var type;
var seed;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Est-ce que ça nous sert de stocker les détails des soumissions UVa? L'idée de base était d'uniformiser toutes les soumissions dans un format à nous et ainsi d'éviter de stocker des "détails inutiles" et de devoir faire des tables pour tous les formats supportés. Tout de façon il faut bien les interpréter à un moment, pourquoi ne pas les interpréter dès que possible et jeter l'info inutile ? Je ne dis pas que j'ai pensé à tout, c'est plutôt une réflexion...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve le truc actuel plutôt minimaliste, surtout par rapport aux soumissions Codeforces (exemple: http://codeforces.com/api/user.status?handle=Fefer_Ivan&from=1&count=10). Les soumissions et les problèmes sont déjà uniformisés mais pour la table des utilisateurs UVa il n'y a pas le choix puisqu'il faut gérer la correspondance entre le username et l'id.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mais à un moment, tu devras bien "réduire" ça à qqchose de commun à notre plateforme. Stocker tout chez ne me semble pas nécessaire, mais tu veux essayer, vas-y.

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};

exports.up = function(db) {

return db.createTable('uva_users', {
uva_user_id: { type: 'serial', primaryKey: true },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le premier attribut est un identifiant propre à nous ? Alors il faudrait plutôt utiliser comme clé la clé étrangère de users directement.
Si tu veux utiliser une table extérieure, il faut également virer les champs qui y étaient dédiés dans users.

Copy link
Member Author

@Technici4n Technici4n Jun 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour moi, la table users contient les utilisateurs "privilégiés" pour lesquels on veut calculer les scores. Les différents services ont des besoins différents, et essayer de tout caser dans une seule table m'a l'air très foireux. Que doit-il se passer en cas de requête sans username Codeforces d'un utilisateur qui n'avait pas encore fait de requête, par exemple ? Il faut aller chercher les soumissions sur UVa et ensuite, il faut les stocker où ? Et on fait quoi des champs Codeforces ?

Je ne sais pas si cet attribut a une grande utilité, je vais regarder si je dois le virer.

Copy link
Member Author

@Technici4n Technici4n Jun 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cela dit, je n'ai pas encore viré les champs inutiles des autres tables. Et je n'ai pas encore rajouté les indexes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne suis pas à 100% certains d'avoir compris ta question mais l'idée était de parser de temps en temps les updates UVa et Codeforces pour chaque utilisateur, d'insérer les avancements relevants pour nous (c'est-à-dire liés à une tâche définie dans topic_tasks ) dans activities. Et d'utiliser ça après pour faire la mise à jour continue des scores.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pensais stocker les infos de tous les utilisateurs qui ont sollicité la plateforme, et d'utiliser ces infos pour les quelques utilisateurs privilégiés (à savoir ceux qui participent à beCP) afin de mettre à jour leurs scores à intervalles réguliers.

Je me dis que la plateforme peut servir comme uniformisation des services uHunt, Codeforces, ... et qu'elle peut en plus nous permettre de calculer les scores relatifs aux différents contestants belges.
Uniformiser les soumissions pourrait me permettre d'intégrer plus de plateformes sur la page d'accueil de mon site sans difficultés (http://beoi-training.herokuapp.com/) et ainsi d'avoir plus de visibilité sur ce que les autres font. (Et si un jour quelqu'un veut faire un site web similaire il aura déjà de bons outils disponibles)

uva_id: { type: 'int' },
uva_username: { type: 'string', length: 50, notNull: true, unique: true },
}).then(
function(result) {
db.runSql(
'CREATE TABLE submissions (\n' +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pq ne pas avoir utilisé db.createTable ? ce serait plus lisible et consistent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parce que je ne pense pas qu'il supporte une primary key à plusieurs colonnes, en tout cas je n'ai pas trouvé.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si si, il faut juste le définir séparément la clé comme tu fais en SQL. Cfr http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html#label-primary_key

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah oui, je ne sais pas pq je t'ai envoyé sur le migrateur de Sequel ruby ;-)
Mais si il le supporte vu que je l'ai fait dans la première migration, table scores

'platform varchar(50),\n' +
'platform_submission_id integer,\n' +
'platform_problem_id varchar(50) NOT NULL,\n' +
'username text NOT NULL,\n' +
'verdict varchar(50) NOT NULL,\n' +
'language varchar(50) NOT NULL,\n' +
'runtime integer,\n' +
'time bigint,\n' +
'primary key (platform, platform_submission_id)\n' +
');\n'
);
}
).then(
function(result) {
db.runSql(
'CREATE TABLE problems (\n' +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ce contenu était déjà prévu dans tasks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pensais que tasks contiendrait les exercices assignés par les coaches. Et il n'y a pas de titres en français ni en néerlandais.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmh oui le but est stocker tous les exercices qui ont de la valeur pour l'évaluation, donc pas tous les exercices mais bcp, triés par topic etc. Mais donc l'idée serait d'avoir des traductions fr/nl qui arrivent à un moment, mais pas nécessaire.

'platform varchar(50),\n' +
'platform_problem_id varchar(50),\n' +
'platform_extra_data varchar,\n' +
'title varchar NOT NULL,\n' +
'primary key (platform, platform_problem_id)\n' +
');\n'
);
}
);
};

exports.down = function(db) {
return db.dropTable('uva_users').
then( function(result) {
db.dropTable('submissions');
}).then( function(result) {
db.dropTable('problems');
});
};

exports._meta = {
"version": 1
};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
"db-migrate": "^0.10.0-beta.20",
"db-migrate-pg": "^0.1.11",
"pg": "^6.1.4",
"serverless-secrets-plugin": "0.0.1"
"serverless-secrets-plugin": "0.0.1",
"request": "*",
"lodash": "*",
"sprintf-js": "*"
},
"devDependencies": {
"serverless": "^1.10.0"
"serverless": "^1.10.0",
"serverless-mocha-plugin": "^1.3.5"
}
}
Loading