Commit 547146b2 by Иван Кубота

Add lots of tags

General questionBits: clustering by tags getUniqComponents(Products[]) subdivideComponents(from: Products[], sub: Products[]) 2 new question types Multiword intersecting filtering in UI FLAG for using subsubcategory instead of tags D.join now pass index to called fn
parent d7c6d211
Pipeline #511 canceled with stage
...@@ -1018,5 +1018,575 @@ ...@@ -1018,5 +1018,575 @@
"type": 0, "type": 0,
"eid": 78, "eid": 78,
"tag": 29 "tag": 29
},
{
"cid": 173,
"type": 0,
"eid": 1,
"tag": 30
},
{
"cid": 174,
"type": 0,
"eid": 2,
"tag": 30
},
{
"cid": 175,
"type": 0,
"eid": 3,
"tag": 30
},
{
"cid": 176,
"type": 0,
"eid": 4,
"tag": 30
},
{
"cid": 177,
"type": 0,
"eid": 5,
"tag": 30
},
{
"cid": 178,
"type": 0,
"eid": 1,
"tag": 31
},
{
"cid": 179,
"type": 0,
"eid": 3,
"tag": 31
},
{
"cid": 180,
"type": 0,
"eid": 3,
"tag": 32
},
{
"cid": 181,
"type": 0,
"eid": 31,
"tag": 32
},
{
"cid": 182,
"type": 0,
"eid": 34,
"tag": 32
},
{
"cid": 183,
"type": 0,
"eid": 35,
"tag": 32
},
{
"cid": 184,
"type": 0,
"eid": 58,
"tag": 32
},
{
"cid": 185,
"type": 0,
"eid": 61,
"tag": 32
},
{
"cid": 186,
"type": 0,
"eid": 70,
"tag": 32
},
{
"cid": 187,
"type": 0,
"eid": 73,
"tag": 32
},
{
"cid": 188,
"type": 0,
"eid": 74,
"tag": 32
},
{
"cid": 189,
"type": 0,
"eid": 1,
"tag": 33
},
{
"cid": 190,
"type": 0,
"eid": 2,
"tag": 33
},
{
"cid": 191,
"type": 0,
"eid": 11,
"tag": 33
},
{
"cid": 192,
"type": 0,
"eid": 15,
"tag": 33
},
{
"cid": 193,
"type": 0,
"eid": 43,
"tag": 33
},
{
"cid": 195,
"type": 0,
"eid": 64,
"tag": 33
},
{
"cid": 196,
"type": 0,
"eid": 70,
"tag": 33
},
{
"cid": 197,
"type": 0,
"eid": 77,
"tag": 33
},
{
"cid": 198,
"type": 0,
"eid": 87,
"tag": 33
},
{
"cid": 199,
"type": 0,
"eid": 11,
"tag": 34
},
{
"cid": 200,
"type": 0,
"eid": 86,
"tag": 34
},
{
"cid": 201,
"type": 0,
"eid": 87,
"tag": 34
},
{
"cid": 202,
"type": 0,
"eid": 88,
"tag": 34
},
{
"cid": 203,
"type": 0,
"eid": 96,
"tag": 34
},
{
"cid": 204,
"type": 0,
"eid": 99,
"tag": 34
},
{
"cid": 205,
"type": 0,
"eid": 6,
"tag": 6
},
{
"cid": 206,
"type": 0,
"eid": 8,
"tag": 6
},
{
"cid": 207,
"type": 0,
"eid": 9,
"tag": 6
},
{
"cid": 208,
"type": 0,
"eid": 10,
"tag": 6
},
{
"cid": 209,
"type": 0,
"eid": 7,
"tag": 35
},
{
"cid": 210,
"type": 0,
"eid": 100,
"tag": 35
},
{
"cid": 211,
"type": 0,
"eid": 9,
"tag": 36
},
{
"cid": 212,
"type": 0,
"eid": 6,
"tag": 37
},
{
"cid": 213,
"type": 0,
"eid": 44,
"tag": 37
},
{
"cid": 214,
"type": 0,
"eid": 44,
"tag": 34
},
{
"cid": 215,
"type": 0,
"eid": 98,
"tag": 38
},
{
"cid": 216,
"type": 0,
"eid": 5,
"tag": 39
},
{
"cid": 217,
"type": 0,
"eid": 98,
"tag": 39
},
{
"cid": 218,
"type": 0,
"eid": 8,
"tag": 40
},
{
"cid": 219,
"type": 0,
"eid": 85,
"tag": 41
},
{
"cid": 220,
"type": 0,
"eid": 85,
"tag": 42
},
{
"cid": 221,
"type": 0,
"eid": 85,
"tag": 43
},
{
"cid": 222,
"type": 0,
"eid": 85,
"tag": 44
},
{
"cid": 223,
"type": 0,
"eid": 85,
"tag": 45
},
{
"cid": 224,
"type": 0,
"eid": 13,
"tag": 8
},
{
"cid": 225,
"type": 0,
"eid": 13,
"tag": 26
},
{
"cid": 226,
"type": 0,
"eid": 13,
"tag": 46
},
{
"cid": 227,
"type": 0,
"eid": 13,
"tag": 9
},
{
"cid": 228,
"type": 0,
"eid": 7,
"tag": 6
},
{
"cid": 229,
"type": 0,
"eid": 12,
"tag": 47
},
{
"cid": 230,
"type": 0,
"eid": 12,
"tag": 48
},
{
"cid": 231,
"type": 0,
"eid": 11,
"tag": 49
},
{
"cid": 232,
"type": 0,
"eid": 15,
"tag": 49
},
{
"cid": 233,
"type": 0,
"eid": 17,
"tag": 50
},
{
"cid": 234,
"type": 0,
"eid": 19,
"tag": 50
},
{
"cid": 235,
"type": 0,
"eid": 76,
"tag": 51
},
{
"cid": 236,
"type": 0,
"eid": 76,
"tag": 52
},
{
"cid": 237,
"type": 0,
"eid": 76,
"tag": 53
},
{
"cid": 238,
"type": 0,
"eid": 14,
"tag": 48
},
{
"cid": 239,
"type": 0,
"eid": 14,
"tag": 53
},
{
"cid": 240,
"type": 0,
"eid": 16,
"tag": 54
},
{
"cid": 241,
"type": 0,
"eid": 16,
"tag": 50
},
{
"cid": 242,
"type": 0,
"eid": 18,
"tag": 50
},
{
"cid": 243,
"type": 0,
"eid": 20,
"tag": 6
},
{
"cid": 244,
"type": 0,
"eid": 22,
"tag": 55
},
{
"cid": 245,
"type": 0,
"eid": 24,
"tag": 55
},
{
"cid": 246,
"type": 0,
"eid": 25,
"tag": 55
},
{
"cid": 247,
"type": 0,
"eid": 23,
"tag": 47
},
{
"cid": 248,
"type": 0,
"eid": 14,
"tag": 56
},
{
"cid": 249,
"type": 0,
"eid": 32,
"tag": 57
},
{
"cid": 250,
"type": 0,
"eid": 33,
"tag": 57
},
{
"cid": 251,
"type": 0,
"eid": 57,
"tag": 57
},
{
"cid": 252,
"type": 0,
"eid": 59,
"tag": 57
},
{
"cid": 253,
"type": 0,
"eid": 63,
"tag": 57
},
{
"cid": 254,
"type": 0,
"eid": 73,
"tag": 57
},
{
"cid": 255,
"type": 0,
"eid": 31,
"tag": 58
},
{
"cid": 256,
"type": 0,
"eid": 32,
"tag": 58
},
{
"cid": 257,
"type": 0,
"eid": 33,
"tag": 58
},
{
"cid": 258,
"type": 0,
"eid": 34,
"tag": 58
},
{
"cid": 259,
"type": 0,
"eid": 35,
"tag": 58
},
{
"cid": 260,
"type": 0,
"eid": 26,
"tag": 59
},
{
"cid": 261,
"type": 0,
"eid": 27,
"tag": 60
},
{
"cid": 262,
"type": 0,
"eid": 28,
"tag": 60
},
{
"cid": 263,
"type": 0,
"eid": 27,
"tag": 61
},
{
"cid": 264,
"type": 0,
"eid": 28,
"tag": 61
},
{
"cid": 265,
"type": 0,
"eid": 100,
"tag": 61
},
{
"cid": 266,
"type": 0,
"eid": 30,
"tag": 62
},
{
"cid": 267,
"type": 0,
"eid": 30,
"tag": 63
},
{
"cid": 268,
"type": 0,
"eid": 30,
"tag": 64
} }
] ]
\ No newline at end of file
...@@ -114,5 +114,141 @@ ...@@ -114,5 +114,141 @@
{ {
"id": 29, "id": 29,
"name": "суп" "name": "суп"
},
{
"id": 30,
"name": "блины"
},
{
"id": 31,
"name": "ветчина"
},
{
"id": 32,
"name": "свинина"
},
{
"id": 33,
"name": "курица"
},
{
"id": 34,
"name": "яйцо"
},
{
"id": 35,
"name": "малина"
},
{
"id": 36,
"name": "черника"
},
{
"id": 38,
"name": "смородина"
},
{
"id": 39,
"name": "постное"
},
{
"id": 40,
"name": "слива"
},
{
"id": 41,
"name": "сернослив"
},
{
"id": 42,
"name": "свекла"
},
{
"id": 43,
"name": "яблоко"
},
{
"id": 44,
"name": "арахис"
},
{
"id": 45,
"name": "мята"
},
{
"id": 46,
"name": "котлеты"
},
{
"id": 47,
"name": "креветки"
},
{
"id": 48,
"name": "макароны"
},
{
"id": 49,
"name": "капуста"
},
{
"id": 50,
"name": "хлеб"
},
{
"id": 51,
"name": "борщ"
},
{
"id": 52,
"name": "первое"
},
{
"id": 53,
"name": "готовая еда"
},
{
"id": 54,
"name": "кунжут"
},
{
"id": 55,
"name": "кальмар"
},
{
"id": 56,
"name": "лосось"
},
{
"id": 57,
"name": "индейка"
},
{
"id": 58,
"name": "колбаса"
},
{
"id": 59,
"name": "зелень"
},
{
"id": 60,
"name": "зеленый горошек"
},
{
"id": 61,
"name": "кукуруза"
},
{
"id": 62,
"name": "лук"
},
{
"id": 63,
"name": "перец"
},
{
"id": 64,
"name": "сельдерей"
} }
] ]
\ No newline at end of file
...@@ -37,8 +37,8 @@ ...@@ -37,8 +37,8 @@
<script src="js/view/page/components.js"></script> <script src="js/view/page/components.js"></script>
<script src="js/controller/exportLogic.js"></script> <script src="js/controller/exportLogic.js"></script>
<script src="js/controller/quizGenerator.js"></script>
<script src="js/controller/quizBits/main.js"></script> <script src="js/controller/quizBits/main.js"></script>
<script src="js/controller/quizGenerator.js"></script>
......
...@@ -67,12 +67,18 @@ const qB = { ...@@ -67,12 +67,18 @@ const qB = {
}); });
similarClusters = Object.values(similarTags) similarClusters = Object.keys(similarTags)
.filter( a => a.length >= minAmount); .map(k=>({k, v:similarTags[k]}))
.filter( a => a.v.length >= minAmount);
log.push('Similar clustered components by tags: '+similarClusters.title);
filtered = rand(similarClusters); log.push(`Clusters that have >= ${minSimilarTags} similar tags: `+Object.values(similarClusters).length);
let randed = rand(similarClusters);
log.push(`Used cluster tags: ${randed.k.split(',').map(id=>dP.tagsHash[id].name).join(', ')}`);
filtered = randed.v;
} }
...@@ -96,9 +102,72 @@ const qB = { ...@@ -96,9 +102,72 @@ const qB = {
return result; return result;
}, },
subdivideComponents: function(from, sub) {
const hash = {};
from.forEach(c=>hash[normalizeText(c.name)] = c);
sub.forEach(c=>delete hash[normalizeText(c.name)]);
return Object.values(hash);
},
getUniqComponents: function(products) {
const hash = {};
products.forEach(
p=>
p
.getComponents()
.forEach(c=>
hash[normalizeText(c.name)] = c
)
);
return Object.values(hash);
},
prebuild: {
similarTaggedProducts: {
// minimal components in product
questionCmpAmount: 4,
// amount of matched products
products: {from: 4, to: 8},
// product similarity (minimal matched tags count)
minSimilarTags: 2,
getComponents: function(product, ) { fn(log){
let products = qB.randomProduct({
minComponents: this.questionCmpAmount,
amount: this.products,
connectedByTags: true,
minSimilarTags: this.minSimilarTags
}, log);
for( let i = 0, _i = products.length; i < _i; i++ ){
const product = products[ i ],
cmps = product
.getComponents()
.filter(cmp => products.filter(p=>p.containsComponent(cmp)).length === 1)
if(cmps.length>=this.questionCmpAmount){
log.push(`Product ${lapk(product.title)} have >= ${this.questionCmpAmount} uniq components (${cmps.length}):`);
const shuffled = shuffle(cmps.slice()).slice(0, this.questionCmpAmount);
cmps.forEach(c=>log.push(`${shuffled.indexOf(c)>-1?'+':' '} > ${c.name}`));
let wrong = products.slice();
wrong.splice(i,1);
return {
correct: product,
wrong: wrong,
uniq: shuffled
}
}
}
return false;
}
}
} }
}; };
\ No newline at end of file
...@@ -41,50 +41,62 @@ const quizTypes = { ...@@ -41,50 +41,62 @@ const quizTypes = {
questionCmpAmount: 4, questionCmpAmount: 4,
probability: 10, probability: 10,
type: 'Product contains 4 components', type: 'Product contains 4 components',
from(log){ products: {from: 4, to: 8},
let products = qB.randomProduct({ minSimilarTags: 1,
minComponents: this.questionCmpAmount, from: qB.prebuild.similarTaggedProducts.fn,
amount: {from: 4, to: 8}, question(income, log){
connectedByTags: true, return 'В какой из продуктов входят данные ингредиенты: '+income.uniq.map(a=> textFormat(a.name)).join(', ');//products.map(p=>p.title)
minSimilarTags: 2 },
}, log); answer( income, log ){
return shuffle(
for( let i = 0, _i = products.length; i < _i; i++ ){ [new Answer.Correct(textFormat(income.correct.title))]
const product = products[ i ], .concat(income.wrong.map(p=>new Answer.Wrong(textFormat(p.title)))))
cmps = product
.getComponents()
.filter(cmp => products.filter(p=>p.containsComponent(cmp)).length === 1)
if(cmps.length>=this.questionCmpAmount){
log.push(`Product ${lapk(product.title)} have >= ${this.questionCmpAmount} uniq components (${cmps.length}):`);
const shuffled = shuffle(cmps.slice()).slice(0, this.questionCmpAmount);
cmps.forEach(c=>log.push(`${shuffled.indexOf(c)>-1?'+':' '} > ${c.name}`));
let wrong = products.slice();
wrong.splice(i,1);
return {
correct: product,
wrong: wrong,
uniq: shuffled
}
}
} }
shuffle(products); },
{
questionCmpAmount: 2,
probability: 1011,
type: 'Component in product',
products: {from: 4, to: 6},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Выберите один ингредиент, который входит в продукт ${lapk(income.correct.title)}`
},
answer( income, log ){
console.log(products.map(p=>p.getComponents())) return shuffle(
[new Answer.Correct(textFormat(shuffle(income.uniq)[0].name))]
.concat(
shuffle(
qB.subdivideComponents( qB.getUniqComponents(income.wrong), income.correct.getComponents() )
)
.slice(0, rand(this.products.from-1, this.products.to-1))
.map(c=>new Answer.Wrong(textFormat(c.name)))
)
)
}
}, },
{
questionCmpAmount: 2,
probability: 10,
type: 'Select product description',
products: {from: 3, to: 5},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){ question(income, log){
return 'В какой из продуктов входят данные ингредиенты: '+income.uniq.map(a=> textFormat(a.name)).join(', ');//products.map(p=>p.title) return `Какое из этих описаний относится к продукту ${lapk(income.correct.title)}`
}, },
answer( income, log ){ answer( income, log ){
return shuffle( return shuffle(
[new Answer.Correct(textFormat(income.correct.title))] [new Answer.Correct(textFormat(income.correct.description))]
.concat(income.wrong.map(c=>new Answer.Wrong(textFormat(c.title))))) .concat(
income.wrong.map(
p => new Answer.Wrong(textFormat(p.description))
)
))
} }
}, },
{ {
...@@ -226,7 +238,7 @@ const quizGenerator = function(type, photo, subType) { ...@@ -226,7 +238,7 @@ const quizGenerator = function(type, photo, subType) {
return false; return false;
} }
answers = shuffle(cfg.answer.call(cfg, source, log)); let answers = shuffle(cfg.answer.call(cfg, source, log));
if(answers === false){ if(answers === false){
// давай по новой // давай по новой
return quizGenerator( type, photo ); return quizGenerator( type, photo );
......
...@@ -6,15 +6,15 @@ const textFilterMatched = function(where, what) { ...@@ -6,15 +6,15 @@ const textFilterMatched = function(where, what) {
textCache[what] = prepared = tokens.map(token => token[0] === '-' ? {has: false, text: token.substr(1)}: {has: true, text: token}); textCache[what] = prepared = tokens.map(token => token[0] === '-' ? {has: false, text: token.substr(1)}: {has: true, text: token});
} }
var yep = false; var yep = true;
for( var i = 0, _i = prepared.length; i < _i; i++ ){ for( var i = 0, _i = prepared.length; i < _i; i++ ){
var preparedElement = prepared[ i ]; var preparedElement = prepared[ i ];
var matched = where.indexOf(preparedElement.text)>-1; var matched = where.indexOf(preparedElement.text)>-1;
if(preparedElement.has === false && matched){ if(preparedElement.has === false && matched){
return false return false
} }
if(matched) if(!matched)
yep = true; yep = false;
} }
return yep; return yep;
...@@ -65,7 +65,7 @@ const textFormat = function(text, html) { ...@@ -65,7 +65,7 @@ const textFormat = function(text, html) {
.replace(/([а-я\?\.])(\d+\.)/g, '$1 $2') .replace(/([а-я\?\.])(\d+\.)/g, '$1 $2')
.replace(/ [-–—] /g, '&nbsp;— ') .replace(/ [-–—] /g, (html?'&nbsp;':' ')+'— ')
.replace(/-(\d)/g, '–$1') .replace(/-(\d)/g, '–$1')
...@@ -88,7 +88,7 @@ const textFormat = function(text, html) { ...@@ -88,7 +88,7 @@ const textFormat = function(text, html) {
return text.length > 2 || text === 'НЕ' ? '<span class="important">'+text.toLowerCase()+'</span>': text; return text.length > 2 || text === 'НЕ' ? '<span class="important">'+text.toLowerCase()+'</span>': text;
}) })
.replace(/, ([А-Я])/g, (f, a)=>', '+a.toLowerCase()) [html?'replace':'trim'](/, ([А-Я])/g, (f, a)=>', '+a.toLowerCase())
.replace(/\|\|\|\|\|/g,'\n\n') .replace(/\|\|\|\|\|/g,'\n\n')
[html?'replace':'trim'](/\*\*([^*]+)\*\*/g,'<span class="notificate-text">$1</span>') [html?'replace':'trim'](/\*\*([^*]+)\*\*/g,'<span class="notificate-text">$1</span>')
......
const useSubSub = true;
const dataProvider = { const dataProvider = {
maxConnection: 0, maxConnection: 0,
maxTagID: 0, maxTagID: 0,
...@@ -33,19 +34,30 @@ const dataProvider = { ...@@ -33,19 +34,30 @@ const dataProvider = {
(dP.componentsListHashByProduct[cmp.id] || (dP.componentsListHashByProduct[cmp.id] = [])).push(cmp); (dP.componentsListHashByProduct[cmp.id] || (dP.componentsListHashByProduct[cmp.id] = [])).push(cmp);
} }
dP.tagsHash = data.tags.reduce((store, item)=>{ if(useSubSub){
dP.maxTagID = Math.max(dP.maxTagID, item.id); data.tags = [];
store[item.id] = item; data.connections = [];
return store dP.tagsHash = {};
}, {});
Object.values( data.products ).forEach( function( item ){
const tag = dP.Tag.getOrCreate( item.subsubcat );
dP.Tag.connectToProduct( dP.Product.getByID( item.id ), tag );
} );
}else{
data.connections.forEach((connection) => { dP.tagsHash = data.tags.reduce( ( store, item ) => {
dP.maxConnection = Math.max(dP.maxConnection, connection.cid); dP.maxTagID = Math.max( dP.maxTagID, item.id );
if(connection.type === 0){ store[ item.id ] = item;
data.products[connection.eid].tags.push(connection.tag) return store
} }, {} );
});
data.connections.forEach( ( connection ) => {
dP.maxConnection = Math.max( dP.maxConnection, connection.cid );
if( connection.type === 0 ){
data.products[ connection.eid ].tags.push( connection.tag )
}
} );
}
}; };
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
var out = [], isFn = typeof delimiter === 'function'; var out = [], isFn = typeof delimiter === 'function';
for( var i = 0, _i = arr.length - 1; i < _i; i++ ){ for( var i = 0, _i = arr.length - 1; i < _i; i++ ){
out.push(arr[i], isFn?delimiter():delimiter); out.push(arr[i], isFn?delimiter(i):delimiter);
} }
if(i < _i+1) if(i < _i+1)
out.push(arr[i]); out.push(arr[i]);
......
...@@ -21,7 +21,7 @@ view.cmp.Table.prototype = { ...@@ -21,7 +21,7 @@ view.cmp.Table.prototype = {
afterFilter: ()=>true, afterFilter: ()=>true,
filter: function(text) { filter: function(text) {
this.filterText = text; this.filterText = text;
this.filterRegExp = new RegExp(text.replace(/[\[\]\(\)\?\*\.\+]/g,''), 'ig') this.filterRegExp = new RegExp(text.replace(/[\[\]\(\)\?\*\.\+]/g,'').replace(/\s+/g,'|'), 'ig')
}, },
fetch: function() { fetch: function() {
const field = Object.keys(this.sort[0])[0] const field = Object.keys(this.sort[0])[0]
...@@ -65,8 +65,13 @@ view.cmp.Table.prototype = { ...@@ -65,8 +65,13 @@ view.cmp.Table.prototype = {
highlight: function(text) { highlight: function(text) {
if(!this.filterText) if(!this.filterText)
return text; return text;
const txts = [];
;
return D.join(text.split(this.filterRegExp), ()=>D.span({cls: 'highlighted'}, this.filterText)) return D.join(text.replace(this.filterRegExp, function(match) {
txts.push(match);
return '|||||'
}).split('|||||'), (i)=>D.span({cls: 'highlighted'}, txts[i]))
}, },
getActionDelegate: function() { getActionDelegate: function() {
var me = this; var me = this;
......
...@@ -11,15 +11,18 @@ view.cmp.Answer = function(answer, type) { ...@@ -11,15 +11,18 @@ view.cmp.Answer = function(answer, type) {
view.page.Generate = function() { view.page.Generate = function() {
const update = function() { const update = function() {
const photo = store.get('generatePhoto') === 'photo', const photo = store.get('generatePhoto') === 'photo',
type = store.get('generateType'), type = store.get('generateType');
result = quizGenerator(type, photo); try{
const result = quizGenerator( type, photo );
title.innerHTML = textFormat(result.question);
debug.value = result.log.join('\n') title.innerHTML = textFormat( result.question );
D.removeChildren(answers); debug.value = result.log.join( '\n' )
D.appendChild(answers, result.answers.map((a)=>view.cmp.Answer(a, type))); D.removeChildren( answers );
D.appendChild( answers, result.answers.map( ( a ) => view.cmp.Answer( a, type ) ) );
}catch( e ){
console.error(e)
}
}; };
let title, answers, debug; let title, answers, debug;
......
...@@ -125,7 +125,7 @@ view.page.Products = function() { ...@@ -125,7 +125,7 @@ view.page.Products = function() {
( item.tags || [] ).map( t => me.highlight( dP.tagsHash[ t ].name ) ).map( view.cmp.Tag ), ( item.tags || [] ).map( t => me.highlight( dP.tagsHash[ t ].name ) ).map( view.cmp.Tag ),
bonus.full && D.html( { cls: 'product-description' }, textFormat(item.description) ), bonus.full && D.html( { cls: 'product-description' }, textFormat(item.description, true) ),
div( div(
{ cls: 'table-item__product-components' }, { cls: 'table-item__product-components' },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment