initial commit
This commit is contained in:
commit
7f1682c04f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
21
LICENSE
Executable file
21
LICENSE
Executable 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.201701010102
Executable file
BIN
db.sqlite.201701010102
Executable file
Binary file not shown.
BIN
db.sqlite.201701012214
Executable file
BIN
db.sqlite.201701012214
Executable file
Binary file not shown.
146
index.js
Executable file
146
index.js
Executable 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
20
package.json
Executable 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
25
sql.js
Executable 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
105
tampermonkey.js
Executable 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
80
views/index.pug
Executable 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
|
Loading…
Reference in New Issue
Block a user