Restore schema in UI after save
This commit is contained in:
parent
decef38f85
commit
975542bff1
@ -39,12 +39,10 @@ function init() {
|
||||
/*
|
||||
* Exporters
|
||||
*/
|
||||
/*
|
||||
var ExporterRegistry = Packages.com.google.refine.exporters.ExporterRegistry;
|
||||
var QSV2Exporter = Packages.org.openrefine.wikidata.exporters.QuickStatements2Exporter;
|
||||
var QSExporter = Packages.org.openrefine.wikidata.exporters.QuickStatementsExporter;
|
||||
|
||||
ExporterRegistry.registerExporter("qsv2", new QSV2());
|
||||
*/
|
||||
ExporterRegistry.registerExporter("quickstatements", new QSExporter());
|
||||
|
||||
/*
|
||||
* Commands
|
||||
|
@ -82,88 +82,10 @@ SchemaAlignment.autoAlign = function() {
|
||||
}
|
||||
};
|
||||
|
||||
SchemaAlignment._batchSearch = function(queries, onDone) {
|
||||
var keys = [];
|
||||
for (var n in queries) {
|
||||
if (queries.hasOwnProperty(n)) {
|
||||
keys.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
var result = {};
|
||||
var args = [];
|
||||
var makeBatch = function(keyBatch) {
|
||||
var batch = {};
|
||||
for (var k = 0; k < keyBatch.length; k++) {
|
||||
var key = keyBatch[k];
|
||||
batch[key] = queries[key];
|
||||
}
|
||||
|
||||
args.push("http://api.freebase.com/api/service/search?" +
|
||||
$.param({ "queries" : JSON.stringify(batch) }) + "&callback=?");
|
||||
|
||||
args.push(null); // no data
|
||||
args.push(function(data) {
|
||||
for (var k = 0; k < keyBatch.length; k++) {
|
||||
var key = keyBatch[k];
|
||||
result[key] = data[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for (var i = 0; i < keys.length; i += 10) {
|
||||
makeBatch(keys.slice(i, i + 10));
|
||||
}
|
||||
|
||||
args.push(function() {
|
||||
onDone(result);
|
||||
});
|
||||
|
||||
Ajax.chainGetJSON.apply(null, args);
|
||||
};
|
||||
|
||||
SchemaAlignment._cleanName = function(s) {
|
||||
return s.replace(/\W/g, " ").replace(/\s+/g, " ").toLowerCase();
|
||||
};
|
||||
|
||||
SchemaAlignment.createNewRootNode = function() {
|
||||
var rootNode = null;
|
||||
var links = [];
|
||||
var columns = theProject.columnModel.columns;
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
var column = columns[i];
|
||||
var target = {
|
||||
nodeType: "cell-as-topic",
|
||||
columnName: column.name,
|
||||
createForNoReconMatch: true
|
||||
};
|
||||
if ((column.reconConfig) &&
|
||||
ReconciliationManager.isFreebaseIdOrMid(column.reconConfig.identifierSpace) &&
|
||||
ReconciliationManager.isFreebaseId(column.reconConfig.schemaSpace) &&
|
||||
(column.reconConfig.type)) {
|
||||
|
||||
target.type = {
|
||||
id: column.reconConfig.type.id,
|
||||
name: column.reconConfig.type.name
|
||||
};
|
||||
}
|
||||
|
||||
if (column.name == theProject.columnModel.keyColumnName) {
|
||||
rootNode = target;
|
||||
} else {
|
||||
links.push({
|
||||
property: null,
|
||||
target: target
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rootNode = rootNode || { nodeType: "cell-as-topic" };
|
||||
rootNode.links = links;
|
||||
|
||||
return rootNode;
|
||||
};
|
||||
|
||||
var SchemaAlignmentDialog = {};
|
||||
|
||||
SchemaAlignmentDialog.launch = function(onDone) {
|
||||
@ -174,28 +96,17 @@ SchemaAlignmentDialog.launch = function(onDone) {
|
||||
this._reset(theProject.overlayModels.wikibaseSchema, true);
|
||||
}
|
||||
|
||||
SchemaAlignment
|
||||
|
||||
SchemaAlignmentDialog._reset = function(schema, initial) {
|
||||
this._originalSchema = schema || { changes: [] };
|
||||
this._schema = cloneDeep(this._originalSchema); // this is what can be munched on
|
||||
|
||||
$('#schema-alignment-statements-container').empty();
|
||||
|
||||
/*
|
||||
this._nodeUIs = [];
|
||||
for (var i = 0; i < this._protograph.rootNodes.length; i++) {
|
||||
this._nodeUIs.push(new SchemaAlignmentDialog.UINode(
|
||||
this,
|
||||
this._protograph.rootNodes[i],
|
||||
this._nodeTable,
|
||||
{
|
||||
expanded: true,
|
||||
mustBeCellTopic: true
|
||||
if (this._schema && this._schema.changes) {
|
||||
for(var i = 0; i != this._schema.changes.length; i++) {
|
||||
this._addItem(this._schema.changes[i]);
|
||||
}
|
||||
}
|
||||
));
|
||||
}*/
|
||||
// TODO
|
||||
|
||||
if (!this._schema.changes.length) {
|
||||
// this._addItem();
|
||||
@ -227,6 +138,16 @@ SchemaAlignmentDialog._save = function(onDone) {
|
||||
);
|
||||
};
|
||||
|
||||
SchemaAlignmentDialog._createDraggableColumn = function(name, reconciled) {
|
||||
var cell = $("<div></div>").addClass('wbs-draggable-column').text(name);
|
||||
if (reconciled) {
|
||||
cell.addClass('wbs-reconciled-column');
|
||||
} else {
|
||||
cell.addClass('wbs-unreconciled-column');
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
SchemaAlignmentDialog._createDialog = function() {
|
||||
var self = this;
|
||||
var frame = $(DOM.loadHTML("wikidata", "scripts/dialogs/schema-alignment-dialog.html"));
|
||||
@ -241,13 +162,8 @@ SchemaAlignmentDialog._createDialog = function() {
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
var column = columns[i];
|
||||
var reconConfig = column.reconConfig;
|
||||
var cell = $("<div></div>").addClass('wbs-draggable-column').text(columns[i].name);
|
||||
console.log(column.reconStats);
|
||||
if (reconConfig && reconConfig.identifierSpace === this._wikibasePrefix && column.reconStats) {
|
||||
cell.addClass('wbs-reconciled-column');
|
||||
} else {
|
||||
cell.addClass('wbs-unreconciled-column');
|
||||
}
|
||||
var cell = SchemaAlignmentDialog._createDraggableColumn(column.name,
|
||||
reconConfig && reconConfig.identifierSpace === this._wikibasePrefix && column.reconStats);
|
||||
this._columnArea.append(cell);
|
||||
}
|
||||
|
||||
@ -294,18 +210,33 @@ SchemaAlignmentDialog._createDialog = function() {
|
||||
SchemaAlignmentDialog._reconService = ReconciliationManager.getServiceFromUrl(url);
|
||||
};
|
||||
|
||||
SchemaAlignmentDialog._addItem = function() {
|
||||
SchemaAlignmentDialog._addItem = function(json) {
|
||||
console.log('addItem')
|
||||
console.log(json);
|
||||
var subject = null;
|
||||
var statementGroups = null;
|
||||
if (json) {
|
||||
subject = json.subject;
|
||||
statementGroups = json.statementGroups;
|
||||
}
|
||||
|
||||
var item = $('<div></div>').addClass('wbs-item');
|
||||
var inputContainer = $('<div></div>').addClass('wbs-item-input').appendTo(item);
|
||||
SchemaAlignmentDialog._initField(inputContainer, "item");
|
||||
SchemaAlignmentDialog._initField(inputContainer, "item", subject);
|
||||
var right = $('<div></div>').addClass('wbs-right').appendTo(item);
|
||||
$('<div></div>').addClass('wbs-statement-group-container').appendTo(right);
|
||||
var toolbar = $('<div></div>').addClass('wbs-toolbar').appendTo(right);
|
||||
$('<a></a>').addClass('wbs-add-statement-group').text('add statement').click(function() {
|
||||
SchemaAlignmentDialog._addStatementGroup(item);
|
||||
SchemaAlignmentDialog._addStatementGroup(item, null);
|
||||
}).appendTo(toolbar);
|
||||
|
||||
if (statementGroups) {
|
||||
for(var i = 0; i != statementGroups.length; i++) {
|
||||
SchemaAlignmentDialog._addStatementGroup(item, statementGroups[i]);
|
||||
}
|
||||
} else {
|
||||
SchemaAlignmentDialog._addStatementGroup(item);
|
||||
}
|
||||
$('#schema-alignment-statements-container').append(item);
|
||||
}
|
||||
|
||||
@ -319,19 +250,35 @@ SchemaAlignmentDialog._itemToJSON = function (item) {
|
||||
statementGroups: lst};
|
||||
};
|
||||
|
||||
SchemaAlignmentDialog._addStatementGroup = function(item) {
|
||||
SchemaAlignmentDialog._addStatementGroup = function(item, json) {
|
||||
console.log('addStatementGroup')
|
||||
console.log(json);
|
||||
var property = null;
|
||||
var statements = null;
|
||||
if (json) {
|
||||
property = json.property;
|
||||
statements = json.statements;
|
||||
}
|
||||
|
||||
var container = item.find('.wbs-statement-group-container').first();
|
||||
var statementGroup = $('<div></div>').addClass('wbs-statement-group');
|
||||
var inputContainer = $('<div></div>').addClass('wbs-prop-input').appendTo(statementGroup);
|
||||
SchemaAlignmentDialog._initField(inputContainer, "property");
|
||||
SchemaAlignmentDialog._initField(inputContainer, "property", property);
|
||||
var right = $('<div></div>').addClass('wbs-right').appendTo(statementGroup);
|
||||
$('<div></div>').addClass('wbs-statement-container').appendTo(right);
|
||||
var toolbar = $('<div></div>').addClass('wbs-toolbar').appendTo(right);
|
||||
$('<a></a>').addClass('wbs-add-statement').text('add value').click(function() {
|
||||
SchemaAlignmentDialog._addStatement(statementGroup);
|
||||
SchemaAlignmentDialog._addStatement(statementGroup, null);
|
||||
}).appendTo(toolbar);
|
||||
container.append(statementGroup);
|
||||
SchemaAlignmentDialog._addStatement(statementGroup);
|
||||
|
||||
if (statements) {
|
||||
for (var i = 0; i != statements.length; i++) {
|
||||
SchemaAlignmentDialog._addStatement(statementGroup, statements[i]);
|
||||
}
|
||||
} else {
|
||||
SchemaAlignmentDialog._addStatement(statementGroup, null);
|
||||
}
|
||||
}
|
||||
|
||||
SchemaAlignmentDialog._statementGroupToJSON = function (statementGroup) {
|
||||
@ -345,7 +292,16 @@ SchemaAlignmentDialog._statementGroupToJSON = function (statementGroup) {
|
||||
};
|
||||
|
||||
|
||||
SchemaAlignmentDialog._addStatement = function(statementGroup) {
|
||||
SchemaAlignmentDialog._addStatement = function(statementGroup, json) {
|
||||
var qualifiers = null;
|
||||
var value = null;
|
||||
console.log('add statement');
|
||||
console.log(json);
|
||||
if (json) {
|
||||
qualifiers = json.qualifiers;
|
||||
value = json.value;
|
||||
}
|
||||
|
||||
var container = statementGroup.find('.wbs-statement-container').first();
|
||||
var statement = $('<div></div>').addClass('wbs-statement');
|
||||
var toolbar1 = $('<div></div>').addClass('wbs-toolbar').appendTo(statement);
|
||||
@ -353,7 +309,7 @@ SchemaAlignmentDialog._addStatement = function(statementGroup) {
|
||||
SchemaAlignmentDialog._removeStatement(statement);
|
||||
}).appendTo(toolbar1);
|
||||
var inputContainer = $('<div></div>').addClass('wbs-target-input').appendTo(statement);
|
||||
SchemaAlignmentDialog._initField(inputContainer, "target");
|
||||
SchemaAlignmentDialog._initField(inputContainer, "target", value);
|
||||
var right = $('<div></div>').addClass('wbs-right').appendTo(statement);
|
||||
$('<div></div>').addClass('wbs-qualifier-container').appendTo(right);
|
||||
var toolbar2 = $('<div></div>').addClass('wbs-toolbar').appendTo(right);
|
||||
@ -366,10 +322,11 @@ SchemaAlignmentDialog._statementToJSON = function (statement) {
|
||||
return {
|
||||
value:SchemaAlignmentDialog._inputContainerToJSON(inputContainer),
|
||||
qualifiers:[],
|
||||
references:[],
|
||||
};
|
||||
};
|
||||
|
||||
SchemaAlignmentDialog._initField = function(inputContainer, mode) {
|
||||
SchemaAlignmentDialog._initField = function(inputContainer, mode, initialValue) {
|
||||
var input = $('<input></input>').appendTo(inputContainer);
|
||||
|
||||
if (this._reconService !== null) {
|
||||
@ -401,6 +358,18 @@ SchemaAlignmentDialog._initField = function(inputContainer, mode) {
|
||||
});
|
||||
}
|
||||
|
||||
var acceptDraggableColumn = function(column) {
|
||||
input.hide();
|
||||
var columnDiv = $('<div></div>').appendTo(inputContainer);
|
||||
column.appendTo(columnDiv);
|
||||
var deleteButton = $(' <img src="images/close.png" />').addClass('wbs-delete-column-button').appendTo(column);
|
||||
deleteButton.attr('alt', 'remove column');
|
||||
deleteButton.click(function () {
|
||||
columnDiv.remove();
|
||||
input.show();
|
||||
});
|
||||
};
|
||||
|
||||
// If it isn't a property, make it droppable
|
||||
if (mode !== "property") {
|
||||
var acceptClass = ".wbs-draggable-column";
|
||||
@ -408,18 +377,12 @@ SchemaAlignmentDialog._initField = function(inputContainer, mode) {
|
||||
acceptClass = ".wbs-reconciled-column";
|
||||
}
|
||||
|
||||
|
||||
inputContainer.droppable({
|
||||
accept: acceptClass,
|
||||
}).on("drop", function (evt, ui) {
|
||||
input.hide();
|
||||
var columnDiv = $('<div></div>').appendTo(inputContainer);
|
||||
var column = ui.draggable.clone().appendTo(columnDiv);
|
||||
var deleteButton = $(' <img src="images/close.png" />').addClass('wbs-delete-column-button').appendTo(column);
|
||||
deleteButton.attr('alt', 'remove column');
|
||||
deleteButton.click(function () {
|
||||
columnDiv.remove();
|
||||
input.show();
|
||||
});
|
||||
var column = ui.draggable.clone();
|
||||
acceptDraggableColumn(column);
|
||||
inputContainer.data("jsonValue", {
|
||||
type : "wbitemvariable",
|
||||
columnName: ui.draggable.text(),
|
||||
@ -431,6 +394,20 @@ SchemaAlignmentDialog._initField = function(inputContainer, mode) {
|
||||
input.removeClass("wbs-accepting-input");
|
||||
});
|
||||
}
|
||||
|
||||
// Init with the provided initial value.
|
||||
console.log('initField');
|
||||
if (initialValue) {
|
||||
console.log(initialValue);
|
||||
console.log(initialValue.type);
|
||||
if (initialValue.type === "wbitemconstant" || initialValue.type === "wbpropconstant") {
|
||||
input.val(initialValue.label);
|
||||
} else if (initialValue.type == "wbitemvariable") {
|
||||
var cell = SchemaAlignmentDialog._createDraggableColumn(initialValue.columnName, true);
|
||||
acceptDraggableColumn(cell);
|
||||
}
|
||||
inputContainer.data("jsonValue", initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
SchemaAlignmentDialog._inputContainerToJSON = function (inputContainer) {
|
||||
|
@ -63,11 +63,11 @@ public class SaveWikibaseSchemaOperation extends AbstractOperation {
|
||||
|
||||
static public class WikibaseSchemaChange implements Change {
|
||||
final protected WikibaseSchema _newSchema;
|
||||
protected WikibaseSchema _oldSchema;
|
||||
protected WikibaseSchema _oldSchema = null;
|
||||
public final static String overlayModelKey = "wikibaseSchema";
|
||||
|
||||
public WikibaseSchemaChange(WikibaseSchema schema) {
|
||||
_newSchema = schema;
|
||||
public WikibaseSchemaChange(WikibaseSchema newSchema) {
|
||||
_newSchema = newSchema;
|
||||
}
|
||||
|
||||
public void apply(Project project) {
|
||||
|
@ -0,0 +1,61 @@
|
||||
package org.openrefine.wikidata.schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONWriter;
|
||||
import org.wikidata.wdtk.datamodel.helpers.Datamodel;
|
||||
import org.wikidata.wdtk.datamodel.interfaces.Reference;
|
||||
import org.wikidata.wdtk.datamodel.interfaces.Snak;
|
||||
import org.wikidata.wdtk.datamodel.interfaces.SnakGroup;
|
||||
|
||||
|
||||
public class WbReferenceExpr extends BiJsonizable {
|
||||
public static final String jsonType = "wbreferences";
|
||||
|
||||
List<WbSnakExpr> snakExprs;
|
||||
|
||||
public WbReferenceExpr(List<WbSnakExpr> snakExprs) {
|
||||
this.snakExprs = snakExprs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFields(JSONWriter writer, Properties options)
|
||||
throws JSONException {
|
||||
writer.key("snaks");
|
||||
writer.array();
|
||||
for (WbSnakExpr expr : snakExprs) {
|
||||
expr.write(writer, options);
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
|
||||
public static WbReferenceExpr fromJSON(JSONObject obj) throws JSONException {
|
||||
JSONArray arr = obj.getJSONArray("snaks");
|
||||
List<WbSnakExpr> lst = new ArrayList<WbSnakExpr>(arr.length());
|
||||
for (int i = 0; i != arr.length(); i++) {
|
||||
lst.add(WbSnakExpr.fromJSON(arr.getJSONObject(i)));
|
||||
}
|
||||
return new WbReferenceExpr(lst);
|
||||
}
|
||||
|
||||
public Reference evaluate(ExpressionContext ctxt) {
|
||||
List<SnakGroup> snakGroups = new ArrayList<SnakGroup>();
|
||||
for (WbSnakExpr expr : snakExprs) {
|
||||
List<Snak> snakList = new ArrayList<Snak>(1);
|
||||
snakList.add(expr.evaluate(ctxt));
|
||||
snakGroups.add(Datamodel.makeSnakGroup(snakList));
|
||||
}
|
||||
return Datamodel.makeReference(snakGroups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonType() {
|
||||
return jsonType;
|
||||
}
|
||||
|
||||
}
|
@ -28,12 +28,15 @@ public class WbStatementExpr extends BiJsonizable {
|
||||
|
||||
private WbValueExpr mainSnakValueExpr;
|
||||
private List<WbSnakExpr> qualifierExprs;
|
||||
private List<WbReferenceExpr> referenceExprs;
|
||||
// TODO: references
|
||||
|
||||
public WbStatementExpr(WbValueExpr mainSnakValueExpr,
|
||||
List<WbSnakExpr> qualifierExprs) {
|
||||
List<WbSnakExpr> qualifierExprs,
|
||||
List<WbReferenceExpr> referenceExprs) {
|
||||
this.mainSnakValueExpr = mainSnakValueExpr;
|
||||
this.qualifierExprs = qualifierExprs;
|
||||
this.referenceExprs = referenceExprs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -51,14 +54,26 @@ public class WbStatementExpr extends BiJsonizable {
|
||||
|
||||
public static WbStatementExpr fromJSON(JSONObject obj) throws JSONException {
|
||||
JSONObject mainSnakObj = obj.getJSONObject("value");
|
||||
JSONArray qualifiersArr = obj.getJSONArray("qualifiers");
|
||||
|
||||
List<WbSnakExpr> qualifierExprs = new ArrayList<WbSnakExpr>();
|
||||
if (obj.has("qualifiers")) {
|
||||
JSONArray qualifiersArr = obj.getJSONArray("qualifiers");
|
||||
for (int i = 0; i != qualifiersArr.length(); i++) {
|
||||
qualifierExprs.add(WbSnakExpr.fromJSON(qualifiersArr.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
|
||||
List<WbReferenceExpr> referenceExprs = new ArrayList<WbReferenceExpr>();
|
||||
if (obj.has("references")) {
|
||||
JSONArray referencesArr = obj.getJSONArray("references");
|
||||
for (int i = 0; i != referencesArr.length(); i++) {
|
||||
referenceExprs.add(WbReferenceExpr.fromJSON(referencesArr.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
return new WbStatementExpr(
|
||||
WbValueExpr.fromJSON(mainSnakObj),
|
||||
qualifierExprs);
|
||||
qualifierExprs,
|
||||
referenceExprs);
|
||||
}
|
||||
|
||||
public static List<SnakGroup> groupSnaks(List<Snak> snaks) {
|
||||
|
@ -30,7 +30,7 @@ public abstract class WbValueExpr extends BiJsonizable {
|
||||
} else if (WbStringConstant.jsonType.equals(type)) {
|
||||
valueExpr = WbStringConstant.fromJSON(obj);
|
||||
} else {
|
||||
throw new JSONException("unknown type for WbValueExpr");
|
||||
throw new JSONException("unknown type '"+type+"' for WbValueExpr");
|
||||
}
|
||||
return valueExpr;
|
||||
}
|
||||
|
@ -10,9 +10,12 @@ import org.json.JSONObject;
|
||||
import org.json.JSONWriter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.wikidata.wdtk.datamodel.interfaces.StatementGroup;
|
||||
|
||||
import com.google.refine.model.OverlayModel;
|
||||
import com.google.refine.model.Project;
|
||||
import com.google.refine.model.Row;
|
||||
import org.openrefine.wikidata.schema.ExpressionContext;
|
||||
|
||||
public class WikibaseSchema implements OverlayModel {
|
||||
|
||||
@ -20,7 +23,7 @@ public class WikibaseSchema implements OverlayModel {
|
||||
|
||||
final protected List<WbChangeExpr> changeExprs = new ArrayList<WbChangeExpr>();
|
||||
|
||||
protected String baseUri;
|
||||
protected String baseUri = "http://www.wikidata.org/entity/";
|
||||
|
||||
@Override
|
||||
public void onBeforeSave(Project project) {
|
||||
@ -52,6 +55,24 @@ public class WikibaseSchema implements OverlayModel {
|
||||
return changeExprs;
|
||||
}
|
||||
|
||||
public List<StatementGroup> evaluate(ExpressionContext ctxt) {
|
||||
List<StatementGroup> result = new ArrayList<StatementGroup>();
|
||||
for (WbChangeExpr changeExpr : changeExprs) {
|
||||
WbItemStatementsExpr expr = (WbItemStatementsExpr)changeExpr;
|
||||
result.addAll(expr.evaluate(ctxt));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<StatementGroup> evaluate(Project project) {
|
||||
List<StatementGroup> result = new ArrayList<StatementGroup>();
|
||||
for (Row row : project.rows) {
|
||||
ExpressionContext ctxt = new ExpressionContext(baseUri, row, project.columnModel);
|
||||
result.addAll(evaluate(ctxt));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static public WikibaseSchema reconstruct(JSONObject o) throws JSONException {
|
||||
JSONArray changeArr = o.getJSONArray("changes");
|
||||
WikibaseSchema schema = new WikibaseSchema();
|
||||
|
Loading…
Reference in New Issue
Block a user