forked from enviPath/enviPy
Current Dev State
This commit is contained in:
662
static/js/pps.js
Normal file
662
static/js/pps.js
Normal file
@ -0,0 +1,662 @@
|
||||
/////////////////////////////////////////
|
||||
// Functions to keep after refactoring //
|
||||
/////////////////////////////////////////
|
||||
|
||||
|
||||
var syncKetcherLock = 0;
|
||||
var ignoreKetcherUpdates = 0
|
||||
var ignoreTextFieldUpdates = 0
|
||||
// The SMILES generated by Ketcher might look different than the one inserted
|
||||
// however we need this for internal comparison to check whether the SMILES taken
|
||||
// from the TextInput generates the same smiles.
|
||||
var SMILES = "";
|
||||
|
||||
function syncTextInputToKetcher(textInputId, ketcherId) {
|
||||
var ketcher = getKetcher(ketcherId);
|
||||
var textInputSMILES = $('#' + textInputId).val();
|
||||
|
||||
data = {
|
||||
"struct": textInputSMILES,
|
||||
"output_format": "chemical/x-mdl-molfile",
|
||||
"options": {
|
||||
"smart-layout": true,
|
||||
"ignore-stereochemistry-errors": true,
|
||||
"mass-skip-error-on-pseudoatoms": false,
|
||||
"gross-formula-add-rsites": true
|
||||
}
|
||||
}
|
||||
|
||||
ketcher.server.layout(data).then(
|
||||
function(response) {
|
||||
if(response.struct != '') {
|
||||
console.log("Writing " + textInputSMILES + " to Ketcher");
|
||||
ketcher.setMolecule(textInputSMILES);
|
||||
|
||||
// Wait for ketcher to accept the change and set global SMILES
|
||||
setTimeout(function() {
|
||||
SMILES = ketcher.getSmiles();
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
).catch(
|
||||
function(res) {
|
||||
console.log("Promise failed" + res);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function syncKetcherToTextInput(ketcherId, textInputId) {
|
||||
// Obtain values
|
||||
var ketcher = getKetcher(ketcherId);
|
||||
// Get the SMILES of the molecule currently drawn in Ketcher
|
||||
var tempKetcherSMILES;
|
||||
try {
|
||||
tempKetcherSMILES = ketcher.getSmiles();
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// compare to SMILES from global scope
|
||||
if(SMILES == tempKetcherSMILES) {
|
||||
// There was no change originating from TextInput
|
||||
return
|
||||
} else {
|
||||
SMILES = tempKetcherSMILES;
|
||||
$("#" + textInputId).val(SMILES);
|
||||
}
|
||||
}
|
||||
|
||||
function syncKetcherAndTextInput(origin, ketcherId, textInputId) {
|
||||
// check if function is "locked"
|
||||
if(origin == 'ketcher') {
|
||||
// Early exit - no op
|
||||
if (ignoreKetcherUpdates == 1) {
|
||||
return;
|
||||
}
|
||||
// Lock updates triggered by textInput
|
||||
ignoreTextFieldUpdates = 1;
|
||||
|
||||
// Do the sync
|
||||
syncKetcherToTextInput(ketcherId, textInputId);
|
||||
|
||||
ignoreTextFieldUpdates = 0;
|
||||
} else {
|
||||
// Early exit - no op
|
||||
if(ignoreTextFieldUpdates == 1) {
|
||||
return;
|
||||
}
|
||||
ignoreKetcherUpdates = 1;
|
||||
|
||||
// Do the sync
|
||||
syncTextInputToKetcher(textInputId, ketcherId);
|
||||
|
||||
ignoreKetcherUpdates = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the ketcher object usually located in an iframe
|
||||
function getKetcher(idToUse){
|
||||
if (idToUse === undefined) {
|
||||
console.log("getKetcher() called without id!");
|
||||
idToUse = 'ifKetcher';
|
||||
}
|
||||
var ketcherFrame = document.getElementById(idToUse);
|
||||
|
||||
if ('contentDocument' in ketcherFrame) {
|
||||
return ketcherFrame.contentWindow.ketcher;
|
||||
} else { // IE7
|
||||
return document.frames[idToUse].window.ketcher;
|
||||
}
|
||||
}
|
||||
|
||||
function enterKeyPressed(event){
|
||||
// 13 means return/enter
|
||||
if (event.which == 13 || event.keyCode == 13) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
};
|
||||
|
||||
// Function to attach an RSS feed available at feedUrl and attaches it to attachElement
|
||||
function loadRSSFeed(attachElement, feedUrl) {
|
||||
$.ajax(feedUrl, {
|
||||
accepts:{
|
||||
xml:"application/rss+xml"
|
||||
},
|
||||
dataType:"xml",
|
||||
success:function(data) {
|
||||
$(data).find("item").each(function () {
|
||||
var el = $(this);
|
||||
|
||||
document.getElementById(attachElement).innerHTML += '<li class="list-group-item">';
|
||||
+ '<h4 class="list-group-item-heading">'
|
||||
+ '<a target="blank" href="' + el.find("link").text() + '">'
|
||||
+ el.find("title").text()
|
||||
+ "</a></h4><h6>"
|
||||
+ el.find("pubDate").text()
|
||||
+ "</h6><p>"
|
||||
+ el.find("description").text()
|
||||
+ "</p></li>";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hasValue(id) {
|
||||
var element = document.getElementById(id);
|
||||
if(element.value === ""
|
||||
|| element.value === undefined
|
||||
|| element.value === null
|
||||
|| element.value === "undefined"){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function beforePredict(){
|
||||
// TODO make it a parameter
|
||||
if(!hasValue("smilesinput")) {
|
||||
return;
|
||||
}
|
||||
|
||||
encodedSMILES = encodeURIComponent($('#smilesinput').val());
|
||||
// TODO Fix via templating to set host
|
||||
url = "/pathway?rootNode="+ encodedSMILES;
|
||||
|
||||
$.getJSON(url, function(result) {
|
||||
// If something is found the returned dict contains the key "pathway" if not it contains "object"
|
||||
if(result.pathway){
|
||||
// TODO set as param
|
||||
attachObjectsAsList("foundPathways", result.pathway);
|
||||
} else {
|
||||
$("#index-form").submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function attachObjectsAsList(elementId, objects){
|
||||
var content ='<ul class="list-group">';
|
||||
var foundone = false;
|
||||
|
||||
for (var obj in objects) {
|
||||
if(objects[obj].reviewStatus == "reviewed"){
|
||||
content += '<a class="list-group-item" href="' + objects[obj].id + '">';
|
||||
content += objects[obj].name;
|
||||
content += '</a>';
|
||||
foundone = true;
|
||||
}
|
||||
}
|
||||
|
||||
content += '</ul>';
|
||||
console.log(foundone);
|
||||
if(foundone){
|
||||
$("#foundPathways").append(content);
|
||||
$("#foundMatching").modal();
|
||||
} else {
|
||||
$("#index-form").submit();
|
||||
}
|
||||
}
|
||||
|
||||
class Setting {
|
||||
constructor(nameInputId, selectedPackagesId, relativeReasoningSelectId, cutoffInputId, evaluationTypeSelectId,
|
||||
availableTSSelectId, formsId, tableId, summaryTableId) {
|
||||
this.nameInputId = nameInputId;
|
||||
this.selectedPackagesId = selectedPackagesId;
|
||||
this.relativeReasoningSelectId = relativeReasoningSelectId;
|
||||
this.cutoffInputId = cutoffInputId;
|
||||
this.evaluationTypeSelectId = evaluationTypeSelectId;
|
||||
this.availableTSSelectId = availableTSSelectId;
|
||||
this.formsId = formsId;
|
||||
this.tableId = tableId;
|
||||
this.summaryTableId = summaryTableId;
|
||||
|
||||
// General settings
|
||||
this.name = null;
|
||||
this.selectedPackages = [];
|
||||
|
||||
// Relative Reasoning related
|
||||
this.selectedRelativeReasoning = null;
|
||||
this.cutoff = null;
|
||||
this.evaluationType = null;
|
||||
|
||||
// parameters such as { "lowPH": 7, "highPH": 8 }
|
||||
this.tsParams = {};
|
||||
}
|
||||
|
||||
extractName() {
|
||||
var tempName = $('#' + this.nameInputId).val()
|
||||
if (tempName == '') {
|
||||
console.log("Name was empty...");
|
||||
return;
|
||||
}
|
||||
this.name = tempName;
|
||||
}
|
||||
|
||||
extractSelectedPackages() {
|
||||
var selPacks = $("#" + this.selectedPackagesId + " :selected");
|
||||
var ref = this;
|
||||
ref.selectedPackages = [];
|
||||
selPacks.each(function () {
|
||||
var obj = {}
|
||||
obj['id'] = this.value;
|
||||
obj['name'] = this.text;
|
||||
ref.selectedPackages.push(obj);
|
||||
});
|
||||
}
|
||||
|
||||
extractRelativeReasoning() {
|
||||
var tempRR = $('#' + this.relativeReasoningSelectId + " :selected").val()
|
||||
if (tempRR == '') {
|
||||
console.log("RR was empty...");
|
||||
return;
|
||||
}
|
||||
var obj = {}
|
||||
obj['id'] = $('#' + this.relativeReasoningSelectId + " :selected").val()
|
||||
obj['name'] = $('#' + this.relativeReasoningSelectId + " :selected").text()
|
||||
this.selectedRelativeReasoning = obj;
|
||||
}
|
||||
|
||||
extractCutoff() {
|
||||
var tempCutoff = $('#' + this.cutoffInputId).val()
|
||||
if (tempCutoff == '') {
|
||||
console.log("Cutoff was empty...");
|
||||
return;
|
||||
}
|
||||
this.cutoff = tempCutoff;
|
||||
}
|
||||
|
||||
extractEvaluationType() {
|
||||
var tempEvaluationType = $('#' + this.evaluationTypeSelectId).val()
|
||||
if (tempEvaluationType == '') {
|
||||
console.log("EvaluationType was empty...");
|
||||
return;
|
||||
}
|
||||
this.evaluationType = tempEvaluationType;
|
||||
}
|
||||
|
||||
addTruncator() {
|
||||
// triggered by "Add"
|
||||
// will extract values and afterwards updates table + summary
|
||||
var type = $("#" + this.availableTSSelectId + " :selected").val();
|
||||
var text = $("#" + this.availableTSSelectId + " :selected").text();
|
||||
var form = $("#" + type + "_form > :input")
|
||||
|
||||
// flag to check whether at least one input had a valid value
|
||||
var addedValue = false;
|
||||
|
||||
// reference being used in each
|
||||
var ref = this;
|
||||
|
||||
form.each(function() {
|
||||
if(this.value == "" || this.value === "undefined"){
|
||||
console.log(this);
|
||||
console.log("Skipping " + this.name);
|
||||
} else {
|
||||
var obj = {}
|
||||
obj[this.name] = this.value;
|
||||
obj['text'] = text;
|
||||
ref.tsParams[type] = obj
|
||||
}
|
||||
});
|
||||
|
||||
this.updateTable();
|
||||
this.updateSummaryTable();
|
||||
}
|
||||
|
||||
removeTruncator(rowId) {
|
||||
var summary = rowId.startsWith("sum") ? true : false;
|
||||
// plain key
|
||||
var key = rowId.replace(summary ? "sum" : "trunc", "").replace("row", "");
|
||||
|
||||
console.log("Removing " + key);
|
||||
|
||||
// remove the rows
|
||||
$("#trunc"+ key + "row").remove();
|
||||
if($("#sum"+ key + "row").length > 0) {
|
||||
$("#sum"+ key + "row").remove();
|
||||
}
|
||||
|
||||
delete this.tsParams[key];
|
||||
}
|
||||
|
||||
updateTable() {
|
||||
// remove all children
|
||||
$('#'+this.tableId + "Body").empty()
|
||||
|
||||
var innerHTML = "<tr>" +
|
||||
"<td>Name</td>" +
|
||||
"<td>Value</td>" +
|
||||
"<td width='10%'>Action</td>" +
|
||||
"</tr>";
|
||||
|
||||
for (var x in this.tsParams) {
|
||||
var val = "";
|
||||
for (var y in this.tsParams[x]){
|
||||
if (y == 'text') {
|
||||
continue;
|
||||
}
|
||||
val += this.tsParams[x][y]
|
||||
|
||||
}
|
||||
innerHTML += "<tr id='trunc" + x + "row'>" +
|
||||
"<td>" + this.tsParams[x]['text'] + "</td>" +
|
||||
"<td>" + val + "</td>" +
|
||||
"<td width='10%'>"+
|
||||
"<button type='button' id='" + x + "button' class='form-control' onclick='s.removeTruncator(\"trunc" + x + "row\")'>Remove</button>" +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
$('#'+this.tableId + "Body").append(innerHTML);
|
||||
}
|
||||
|
||||
packageRows() {
|
||||
var res = '';
|
||||
for(var p in this.selectedPackages) {
|
||||
var obj = this.selectedPackages[p];
|
||||
res += "<tr>" +
|
||||
"<td>Package</td>" +
|
||||
"<td>" + obj['name'] + "</td>" +
|
||||
"<td width='10%'>" +
|
||||
// "<button type='button' id='relativereasoningbutton' class='form-control' onclick='s.removeTruncator(\"Relative Reasoning\")'>Remove</button>" +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
modelRow() {
|
||||
if(this.selectedRelativeReasoning == null) {
|
||||
return '';
|
||||
}
|
||||
return "<tr>" +
|
||||
"<td>Relative Reasoning</td>" +
|
||||
"<td>" + this.selectedRelativeReasoning['name'] + " " + (this.evaluationType == "singleGen" ? "SG" : "MG") + " with t=" + this.cutoff + " </td>" +
|
||||
"<td width='10%'>" +
|
||||
// "<button type='button' id='relativereasoningbutton' class='form-control' onclick='s.removeTruncator(\"Relative Reasoning\")'>Remove</button>" +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
updateSummaryTable() {
|
||||
// remove all children
|
||||
$('#'+this.summaryTableId + "Body").empty()
|
||||
|
||||
var innerHTML = "<tr>" +
|
||||
"<td>Name</td>" +
|
||||
"<td>Value</td>" +
|
||||
"<td width='10%'>Action</td>" +
|
||||
"</tr>";
|
||||
|
||||
innerHTML += this.packageRows();
|
||||
innerHTML += this.modelRow();
|
||||
|
||||
// var innerHTML = ''
|
||||
for (var x in this.tsParams) {
|
||||
var val = "";
|
||||
for (var y in this.tsParams[x]){
|
||||
if (y == 'text') {
|
||||
continue;
|
||||
}
|
||||
val += this.tsParams[x][y]
|
||||
|
||||
}
|
||||
innerHTML += "<tr id='sum" + x + "row'>" +
|
||||
"<td>" + this.tsParams[x]['text'] + "</td>" +
|
||||
"<td>" + val + "</td>" +
|
||||
"<td width='10%'>"+
|
||||
"<button type='button' id='" + x + "button' class='form-control' onclick='s.removeTruncator(\"sum" + x + "row\")'>Remove</button>" +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
$('#'+this.summaryTableId + "Body").append(innerHTML);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
function queryResultToTable(appendId, result) {
|
||||
$('#' + appendId).empty();
|
||||
var table = '<table class="table">' +
|
||||
'<thead class="thead-dark">' +
|
||||
'<tr>';
|
||||
|
||||
var header = '';
|
||||
for (var r in result['result']) {
|
||||
var keys = Object.keys(result['result'][r]);
|
||||
for (var k in keys) {
|
||||
header += '<th>' + keys[k] + '</th>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
table += header;
|
||||
table += '</tr>';
|
||||
table += '</thead>';
|
||||
table += '<tbody>';
|
||||
|
||||
var body = '';
|
||||
for (var r in result['result']) {
|
||||
body += '<tr>';
|
||||
for (var k in result['result'][r]) {
|
||||
body += '<td>' + result['result'][r][k] + '</td>';
|
||||
}
|
||||
body += '</tr>';
|
||||
}
|
||||
|
||||
table += body;
|
||||
table += '</tbody>';
|
||||
table += '</table>';
|
||||
|
||||
$('#' + appendId).append(table);
|
||||
}
|
||||
|
||||
function makeLoadingGif(attachOb, src) {
|
||||
// TODO provide SERVER_BASE from outside
|
||||
|
||||
img_src = src === undefined ? 'http://localhost:8080' : src
|
||||
|
||||
var loadingGif = "<div style='text-align:center;'><div class='loading' align='middle'>"
|
||||
+ "<img id='image-wait' src='" + img_src + "' alt='Loading'" +
|
||||
" title='Loading'" +
|
||||
" style='text-align:left;margin:0px auto;' align='middle'/>"
|
||||
+ "</div></div>";
|
||||
$(attachOb).html(loadingGif);
|
||||
//document.getElementById("image-wait").src = getBasePath() + "/" + "images/wait.gif";
|
||||
}
|
||||
|
||||
function cleanIdName(idName) {
|
||||
return idName.replace(/[ \-()]/g, "")
|
||||
}
|
||||
|
||||
function makeAccordionHead(accordionId, accordionTitle, reviewStatus) {
|
||||
return "<div class='panel-group' id='" + accordionId + "'>"
|
||||
+ "<div class='panel panel-default'>"
|
||||
+ "<div class='panel-heading' id='headingPanel' style='font-size:2rem;height: 46px'>"
|
||||
+ accordionTitle + reviewStatus
|
||||
+ "</div><div id='descDiv'></div>"
|
||||
+ "</div>";
|
||||
}
|
||||
|
||||
function makeAccordionPanel(accordionId, panelName, panelContent, collapsed, id) {
|
||||
if(panelContent == "" || panelContent == "no description") {
|
||||
return "";
|
||||
}
|
||||
if(collapsed == true) {
|
||||
collapsed = "in";
|
||||
} else {
|
||||
collapsed = "";
|
||||
}
|
||||
var panelId = typeof id === "undefined" ? cleanIdName(panelName) : id;
|
||||
return "<div class='panel panel-default panel-heading list-group-item' style='background-color:silver'>"
|
||||
+ "<h4 class='panel-title'>"
|
||||
+ "<a id=" + panelId + "Link" + " data-toggle='collapse' data-parent='#" + accordionId + "' href='#" + panelId + "'>"
|
||||
+ panelName
|
||||
+ "</a>"
|
||||
+ "</h4>"
|
||||
+ "</div>"
|
||||
+ "<div id='" + panelId + "' class='panel-collapse collapse " + collapsed + "'>"
|
||||
+ "<div class='panel-body list-group-item' id='"+panelId+"Content'>"
|
||||
+ panelContent
|
||||
+ "</div>"
|
||||
+ "</div>";
|
||||
}
|
||||
|
||||
function makeSearchList(divToAppend, jsonob) {
|
||||
|
||||
if(jsonob.status){
|
||||
$(divToAppend).append('<div class="alert alert-danger" role="alert"><p>'+"No results..."+'</p></div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var content = makeAccordionHead("searchAccordion", "Results","");
|
||||
|
||||
for ( var type in jsonob){
|
||||
var obj = jsonob[type];
|
||||
var objs = "";
|
||||
for ( var x in obj) {
|
||||
objs += "<a class='list-group-item' href=\"" + obj[x].id + "\">"
|
||||
+ obj[x].name + "</a>";
|
||||
}
|
||||
content += makeAccordionPanel("searchAccordion", type, objs, true);
|
||||
}
|
||||
$(divToAppend).append(content);
|
||||
|
||||
}
|
||||
|
||||
function fillPRCurve(modelUri, onclick){
|
||||
if (modelUri == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// clear div
|
||||
$('#plotDiv').empty();
|
||||
|
||||
// for the time being add animation
|
||||
makeLoadingGif('#plotDiv');
|
||||
|
||||
$.getJSON(modelUri + "?prcurve", function (result) {
|
||||
// remove loading gif
|
||||
$('#plotDiv').empty();
|
||||
|
||||
// check if model is in a proper state
|
||||
if(result.hasOwnProperty('error')){
|
||||
var content = '<div>'+result.error+' \nYou can check the model <a href="' + modelUri + '">here</a>. </div>';
|
||||
$('#plotDiv').append(content);
|
||||
return
|
||||
}
|
||||
|
||||
var chartPanel = '<div id="chart" class="panel-group"></div>';
|
||||
$('#plotDiv').append(chartPanel);
|
||||
|
||||
// Labels
|
||||
var x = ['Recall'];
|
||||
var y = ['Precision'];
|
||||
var thres = ['threshold'];
|
||||
|
||||
// Compare function for the given array
|
||||
function compare(a, b) {
|
||||
if (a.threshold < b.threshold)
|
||||
return -1;
|
||||
else if (a.threshold > b.threshold)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
// gets the index for a certain value
|
||||
function getIndexForValue(data, val, val_name) {
|
||||
for(var idx in data) {
|
||||
if(data[idx][val_name] == val) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
var data = result.prdata;
|
||||
var dataLength = Object.keys(data).length;
|
||||
data.sort(compare);
|
||||
|
||||
// collect data in 3 individual arrays
|
||||
for (var idx in data) {
|
||||
var d = data[idx];
|
||||
x.push(d.recall);
|
||||
y.push(d.precision);
|
||||
thres.push(d.threshold);
|
||||
}
|
||||
|
||||
// generate the actual chart with values collected above
|
||||
var chart = c3.generate({
|
||||
bindto: '#chart',
|
||||
data: {
|
||||
onclick: function (d, e) {
|
||||
var idx = d.index;
|
||||
var thresh = data[dataLength-idx-1].threshold;
|
||||
|
||||
onclick(thresh)
|
||||
|
||||
},
|
||||
x: 'Recall',
|
||||
y: 'Precision',
|
||||
columns: [
|
||||
x,
|
||||
y,
|
||||
//thres
|
||||
]
|
||||
},
|
||||
size: {
|
||||
height: 400, // TODO: Make variable to current modal width
|
||||
width: 480
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
max: 1,
|
||||
min: 0,
|
||||
label: 'Recall',
|
||||
padding: 0,
|
||||
tick: {
|
||||
fit: true,
|
||||
values: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
||||
}
|
||||
},
|
||||
y: {
|
||||
max: 1,
|
||||
min: 0,
|
||||
label: 'Precision',
|
||||
padding: 0,
|
||||
tick: {
|
||||
fit: true,
|
||||
values: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
||||
}
|
||||
}
|
||||
},
|
||||
point: {
|
||||
r: 4
|
||||
},
|
||||
tooltip: {
|
||||
format: {
|
||||
title: function (recall) {
|
||||
idx = getIndexForValue(data, recall, "recall");
|
||||
if(idx != -1) {
|
||||
return "Threshold: " + data[idx].threshold;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
value: function (precision, ratio, id) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user