initial commit

This commit is contained in:
Raido Kalbre 2018-08-25 18:12:15 +03:00
commit 7f1682c04f
10 changed files with 398 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

21
LICENSE Executable file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
db.sqlite Executable file

Binary file not shown.

BIN
db.sqlite.201701010102 Executable file

Binary file not shown.

BIN
db.sqlite.201701012214 Executable file

Binary file not shown.

146
index.js Executable file
View File

@ -0,0 +1,146 @@
var express = require('express');
var app = express();
var cors = require('cors');
var escape = require('escape-html')
var bodyParser = require('body-parser');
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('db.sqlite', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, function (err) {
if(err) {
console.log('error opening db:', err);
} else {
db.run("CREATE TABLE IF NOT EXISTS images (id INTEGER PRIMARY KEY, src TEXT, dataUri TEXT)");
db.run("CREATE TABLE IF NOT EXISTS questions (id INTEGER PRIMARY KEY, updated DATETIME DEFAULT CURRENT_TIMESTAMP, text TEXT, explanation TEXT)");
db.run("ALTER TABLE questions ADD COLUMN explanation TEXT", function (err) {});
db.run("CREATE TABLE IF NOT EXISTS answers (id INTEGER PRIMARY KEY, aid INTEGER, qid INTEGER, text TEXT, correct INTEGER, checked INTEGER)");
db.run("CREATE TABLE IF NOT EXISTS answering (id INTEGER PRIMARY KEY, created DATETIME DEFAULT CURRENT_TIMESTAMP, version TEXT, href TEXT)");
}
});
app.use( bodyParser.json() );
app.use(cors());
app.set('view engine', 'pug');
var corsOptions = {
origin: "*",
methods: "POST"
};
app.get('/', function (req, res) {
db.all("SELECT q.id AS qid, q.text AS question, q.explanation AS explanation, i.dataUri AS image, T2.id AS aid, T2.text AS answer, T2.correct FROM questions q " +
"LEFT JOIN (SELECT qid, MAX(aid) AS maxid FROM answers GROUP BY qid) AS T1 ON q.id = T1.qid " +
"LEFT JOIN answers AS T2 ON T2.aid = T1.maxid AND T2.qid = q.id " +
"LEFT JOIN images AS i ON i.id = q.id " +
"ORDER BY T2.id DESC",
function (err, rows) {
//console.log('selected rows:', rows.length);
if (err) {
console.log('error:', err);
res.sendStatus(500);
}
var prevQid, index = -1;
var questions = rows.reduce(function(questions, row) {
var a = { answer: row.answer, correct: row.correct };
if (prevQid != row.qid) {
questions[++index] = { id: row.qid, question: row.question, explanation: row.explanation, image: row.image, answers: [a] };
} else {
questions[index].answers.push(a);
}
prevQid = row.qid;
return questions;
}, []);
console.log('total questions:', questions.length);
res.render('index', { questions: questions });
});
});
app.post('/explanation/:id', function (req, res) {
var id = req.params.id;
var data = req.body;
if (id && data.explanation) {
console.log('updating', id, 'with', data);
db.run('UPDATE questions SET explanation = $explanation WHERE id = $id', {
$id: id,
$explanation: escape(data.explanation)
}, function (err) {
if (err) {
console.log('error updating explanation for', id, ':', err);
res.sendStatus(500);
} else {
res.sendStatus(200);
}
});
} else {
console.log('invalid explanation for', id, ':', data);
res.sendStatus(400);
}
});
app.post('/qa', cors(corsOptions), function (req, res) {
var data = req.body;
console.log('NEW QA DATA:', JSON.stringify(data));
if (!data.version || !data.data || data.data.length < 15) {
console.log('invalid qa data', !data.version, !data.data, data.data.length < 15);
return res.sendStatus(400);
}
db.serialize(function() {
var aid = null;
db.run("INSERT INTO answering (version, href) VALUES ($version,$href)", {
$version: data.version,
$href: data.href
}, function(err) {
if (err) {
console.log('inserting answering failed:', err);
return;
}
console.log('inserted:', this.changes, 'lastID:', this.lastID);
aid = this.lastID;
if (aid) {
var qstmt = db.prepare("REPLACE INTO questions (id, text) VALUES (?,?)");
var astmt = db.prepare("INSERT INTO answers (aid, qid, text, correct, checked) VALUES (?,?,?,?,?)");
for (var i = 0; i < data.data.length; i++) {
var q = data.data[i];
qstmt.run(q.id, q.question);
for (var j = 0; j < q.answers.length; j++) {
var a = q.answers[j];
astmt.run(aid, q.id, a.text, a.correct, a.checked);
}
}
qstmt.finalize();
astmt.finalize();
res.sendStatus(200);
} else {
console.log('no answering id');
res.sendStatus(400);
}
});
});
});
app.post('/img', cors(corsOptions), function (req, res) {
var data = req.body;
if (!data.version || !data.data.id || !data.data.src || !data.data.dataUri) {
console.log('invalid img data', !data.version, !data.data.id, !data.data.src, !data.data.dataUri);
return res.sendStatus(400);
}
var img = data.data;
console.log('NEW IMG DATA:', JSON.stringify(data).substring(0,200));
var istmt = db.run("REPLACE INTO images (id, src, dataUri) VALUES (?,?,?)", img.id, img.src, img.dataUri, function (err) {
if (err) {
console.log('saving image ' + img.id + 'failed:', err);
res.sendStatus(400);
} else {
res.sendStatus(200);
}
});
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
});

20
package.json Executable file
View File

@ -0,0 +1,20 @@
{
"name": "qadigger",
"version": "0.0.1",
"description": "Tampermonkey script and Node.js Express server to gather questions and answers for later analysis, hobby project",
"main": "index.js",
"scripts": {
"start": "node index",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Raidok",
"license": "MIT",
"dependencies": {
"body-parser": "^1.15.2",
"cors": "^2.8.1",
"escape-html": "^1.0.3",
"express": "^4.14.0",
"pug": "^2.0.0-beta6",
"sqlite3": "^3.1.8"
}
}

25
sql.js Executable file
View File

@ -0,0 +1,25 @@
const CREATE_TABLE_IMAGES = "CREATE TABLE IF NOT EXISTS images (id INTEGER PRIMARY KEY, src TEXT, dataUri TEXT)";
const CREATE_TABLE_QUESTIONS = "CREATE TABLE IF NOT EXISTS questions (id INTEGER PRIMARY KEY, updated DATETIME DEFAULT CURRENT_TIMESTAMP, text TEXT)";
const CREATE_TABLE_ANSWERS = "CREATE TABLE IF NOT EXISTS answers (id INTEGER PRIMARY KEY, aid INTEGER, qid INTEGER, text TEXT, correct INTEGER, checked INTEGER)";
const CREATE_TABLE_ANSWERING = "CREATE TABLE IF NOT EXISTS answering (id INTEGER PRIMARY KEY, created DATETIME DEFAULT CURRENT_TIMESTAMP, version TEXT, href TEXT)";
const SELECT_ALL = "SELECT q.id AS qid, q.text AS question, i.dataUri AS image, T2.id AS aid, T2.text AS answer, T2.correct FROM questions q \
LEFT JOIN (SELECT qid, MAX(aid) AS maxid FROM answers GROUP BY qid) AS T1 ON q.id = T1.qid \
LEFT JOIN answers AS T2 ON T2.aid = T1.maxid AND T2.qid = q.id \
LEFT JOIN images AS i ON i.id = q.id ORDER BY T2.id DESC";
module.exports = {
SELECT_ALL: SELECT_ALL
}
app.get('/top-mistakes', function (req, res) {
db.all("SELECT q.id AS qid, q.text as question, a.acount AS mistakes FROM questions q " +
"JOIN (SELECT qid, count(*) as acount FROM answers WHERE correct != checked GROUP BY aid,qid) AS a ON a.qid = q.id ORDER BY mistakes DESC",
function (err, rows) {
res.render('index', { questions: rows });
});
});

105
tampermonkey.js Executable file
View File

@ -0,0 +1,105 @@
// ==UserScript==
// @name AKparse
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author Raidok
// @match http://localhost:8080/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
if (document.querySelectorAll('td.popup-contents')[0].childNodes[0].textContent.indexOf('viga') === -1) {
console.log('script aborted');
return;
}
function getDataUri(url, callback) {
var image = new Image();
image.onload = function () {
var canvas = document.createElement('canvas');
canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
canvas.getContext('2d').drawImage(this, 0, 0);
console.log('loaded', url);
callback(canvas.toDataURL('image/jpeg'));
};
image.src = url;
console.log('loading', url);
}
function nodeListToArray(list) {
var array= new Array(list.length);
for (var i= 0, n= list.length; i<n; i++)
array[i]= list[i];
return array;
}
function parse() {
return nodeListToArray(document.querySelectorAll('table.box td.box')).map(el => {
var tds = el.querySelectorAll('td');
if (!tds) {
console.log('no tds!');
}
var q = tds[0].innerText;
console.log(q, tds);
var img = tds[1].querySelector('img');
var imgSrc = img ? img.getAttribute('src') : undefined;
var i = imgSrc ? { src: imgSrc } : undefined;
var aHtml = tds[2];
//console.log('vastused:', tds[2].innerHTML);
var id = aHtml.querySelector('input[type=hidden]').getAttribute("value");
var as = nodeListToArray(aHtml.querySelectorAll('input[type=checkbox]')).map(x => {
var checked = !!x.getAttribute('checked');
var green = !!(x.nextElementSibling.getAttribute('style') || '').match(/color:.?green/);
var correct = green ? checked : !checked;
return { checked: checked, green: green, correct: correct, text: x.nextElementSibling.innerText };
});
console.log(q,i,as);
return { id: id, question: q, img: i, answers: as };
});
}
function send(what, data) {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/' + what);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200 || xhr.status === 204) {
console.log('Success! Response:' + xhr.responseText);
} else {
console.log('Request failed. Responded ' + xhr.status + ' with content:' + xhr.responseText);
}
};
xhr.send(JSON.stringify({
version: GM_info.script.version,
href: window.location.href,
userAgent: window.agent,
data: data
}));
}
var data = parse();
send('qa', data);
var itemsProcessed = 0;
data.forEach((item, index, array) => {
if (item.img && item.img.src) {
getDataUri(item.img.src, function(dataUri) {
send('img', { id: item.id, src: item.img.src, dataUri: dataUri });
});
}
});
})();

80
views/index.pug Executable file
View File

@ -0,0 +1,80 @@
html
head
title QA
style(media='screen', type='text/css').
h4 {
margin: 40px 0 10px;
}
textarea {
width: 100%;
height: 50px;
}
.true:hover {
color: green;
}
.false:hover {
color: red;
}
.explanation {
color: #999;
}
script.
function postExplanation(id, explanation) {
if (!id) {
console.log('no id!');
return;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/explanation/' + id);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200 || xhr.status === 204) {
console.log('Success! Response:' + xhr.responseText);
} else {
console.log('Request failed. Responded ' + xhr.status + ' with content:' + xhr.responseText);
}
};
xhr.send(JSON.stringify({explanation: explanation}));
}
function edit(editLink) {
var saveLink = editLink.nextSibling
, textArea = editLink.previousSibling
, div = textArea.previousSibling
, text = div.innerText;
console.log('copying', text);
textArea.value = text;
editLink.setAttribute('style', 'display:none');
saveLink.removeAttribute('style');
textArea.removeAttribute('style');
}
function save(saveLink) {
var id = saveLink.parentNode.getAttribute('data-qid')
, editLink = saveLink.previousSibling
, textArea = editLink.previousSibling
, div = textArea.previousSibling
, text = textArea.value;
console.log('posting', id, text);
div.innerText = text;
editLink.removeAttribute('style');
saveLink.setAttribute('style', 'display:none');
textArea.setAttribute('style', 'display:none');
postExplanation(id, text);
}
body
ul(style='list-style-type:none')
each q in questions
li(data-qid=q.id)
h4= q.question
p.explanation !{ q.explanation ? q.explanation.replace(/\n/g, "<br>") : "" }
textarea(style='display:none')
a(href='javascript:void(0)',onclick='edit(this)') edit
a(href='javascript:void(0)',onclick='save(this)',style='display:none') save
p
img(src=q.image)
ul(style='list-style-type:decimal')
each a in q.answers
li(class=a.correct?'true':'false')= a.answer