Added commands to approve, approve new, and discard recon candidates.

Support sections in menus.

git-svn-id: http://google-refine.googlecode.com/svn/trunk@24 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-02-02 20:50:35 +00:00
parent ac46653aa3
commit 00696a96fc
10 changed files with 514 additions and 149 deletions

View File

@ -14,9 +14,12 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; import org.json.JSONTokener;
import com.metaweb.gridlock.commands.ApproveNewReconcileCommand;
import com.metaweb.gridlock.commands.ApproveReconcileCommand;
import com.metaweb.gridlock.commands.Command; import com.metaweb.gridlock.commands.Command;
import com.metaweb.gridlock.commands.ComputeFacetsCommand; import com.metaweb.gridlock.commands.ComputeFacetsCommand;
import com.metaweb.gridlock.commands.CreateProjectFromUploadCommand; import com.metaweb.gridlock.commands.CreateProjectFromUploadCommand;
import com.metaweb.gridlock.commands.DiscardReconcileCommand;
import com.metaweb.gridlock.commands.DoTextTransformCommand; import com.metaweb.gridlock.commands.DoTextTransformCommand;
import com.metaweb.gridlock.commands.GetColumnModelCommand; import com.metaweb.gridlock.commands.GetColumnModelCommand;
import com.metaweb.gridlock.commands.GetHistoryCommand; import com.metaweb.gridlock.commands.GetHistoryCommand;
@ -43,7 +46,11 @@ public class GridlockServlet extends HttpServlet {
_commands.put("undo-redo", new UndoRedoCommand()); _commands.put("undo-redo", new UndoRedoCommand());
_commands.put("compute-facets", new ComputeFacetsCommand()); _commands.put("compute-facets", new ComputeFacetsCommand());
_commands.put("do-text-transform", new DoTextTransformCommand()); _commands.put("do-text-transform", new DoTextTransformCommand());
_commands.put("reconcile", new ReconcileCommand()); _commands.put("reconcile", new ReconcileCommand());
_commands.put("approve-reconcile", new ApproveReconcileCommand());
_commands.put("approve-new-reconcile", new ApproveNewReconcileCommand());
_commands.put("discard-reconcile", new DiscardReconcileCommand());
} }
@Override @Override

View File

@ -0,0 +1,86 @@
package com.metaweb.gridlock.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.browsing.FilteredRows;
import com.metaweb.gridlock.browsing.RowVisitor;
import com.metaweb.gridlock.history.CellChange;
import com.metaweb.gridlock.history.HistoryEntry;
import com.metaweb.gridlock.history.MassCellChange;
import com.metaweb.gridlock.model.Cell;
import com.metaweb.gridlock.model.Column;
import com.metaweb.gridlock.model.Project;
import com.metaweb.gridlock.model.Recon;
import com.metaweb.gridlock.model.Row;
import com.metaweb.gridlock.model.Recon.Judgment;
import com.metaweb.gridlock.process.QuickHistoryEntryProcess;
public class ApproveNewReconcileCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
List<CellChange> cellChanges = new ArrayList<CellChange>(project.rows.size());
FilteredRows filteredRows = engine.getAllFilteredRows();
filteredRows.accept(project, new RowVisitor() {
int cellIndex;
List<CellChange> cellChanges;
public RowVisitor init(int cellIndex, List<CellChange> cellChanges) {
this.cellIndex = cellIndex;
this.cellChanges = cellChanges;
return this;
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
Cell newCell = new Cell();
newCell.value = cell.value;
newCell.recon = cell.recon != null ? cell.recon.dup() : new Recon();
newCell.recon.match = null;
newCell.recon.judgment = Judgment.New;
CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell);
cellChanges.add(cellChange);
}
return false;
}
}.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges);
HistoryEntry historyEntry = new HistoryEntry(
project, "Approve new topics for " + columnName, massCellChange);
boolean done = project.processManager.queueProcess(
new QuickHistoryEntryProcess(project, historyEntry));
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
} catch (Exception e) {
respondException(response, e);
}
}
}

View File

@ -0,0 +1,86 @@
package com.metaweb.gridlock.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.browsing.FilteredRows;
import com.metaweb.gridlock.browsing.RowVisitor;
import com.metaweb.gridlock.history.CellChange;
import com.metaweb.gridlock.history.HistoryEntry;
import com.metaweb.gridlock.history.MassCellChange;
import com.metaweb.gridlock.model.Cell;
import com.metaweb.gridlock.model.Column;
import com.metaweb.gridlock.model.Project;
import com.metaweb.gridlock.model.Row;
import com.metaweb.gridlock.model.Recon.Judgment;
import com.metaweb.gridlock.process.QuickHistoryEntryProcess;
public class ApproveReconcileCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
List<CellChange> cellChanges = new ArrayList<CellChange>(project.rows.size());
FilteredRows filteredRows = engine.getAllFilteredRows();
filteredRows.accept(project, new RowVisitor() {
int cellIndex;
List<CellChange> cellChanges;
public RowVisitor init(int cellIndex, List<CellChange> cellChanges) {
this.cellIndex = cellIndex;
this.cellChanges = cellChanges;
return this;
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
if (cell.recon != null && cell.recon.candidates.size() > 0) {
Cell newCell = new Cell();
newCell.value = cell.value;
newCell.recon = cell.recon.dup();
newCell.recon.match = newCell.recon.candidates.get(0);
newCell.recon.judgment = Judgment.Approve;
CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell);
cellChanges.add(cellChange);
}
}
return false;
}
}.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges);
HistoryEntry historyEntry = new HistoryEntry(
project, "Approve best recon candidates for " + columnName, massCellChange);
boolean done = project.processManager.queueProcess(
new QuickHistoryEntryProcess(project, historyEntry));
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
} catch (Exception e) {
respondException(response, e);
}
}
}

View File

@ -0,0 +1,82 @@
package com.metaweb.gridlock.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.browsing.FilteredRows;
import com.metaweb.gridlock.browsing.RowVisitor;
import com.metaweb.gridlock.history.CellChange;
import com.metaweb.gridlock.history.HistoryEntry;
import com.metaweb.gridlock.history.MassCellChange;
import com.metaweb.gridlock.model.Cell;
import com.metaweb.gridlock.model.Column;
import com.metaweb.gridlock.model.Project;
import com.metaweb.gridlock.model.Row;
import com.metaweb.gridlock.process.QuickHistoryEntryProcess;
public class DiscardReconcileCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
List<CellChange> cellChanges = new ArrayList<CellChange>(project.rows.size());
FilteredRows filteredRows = engine.getAllFilteredRows();
filteredRows.accept(project, new RowVisitor() {
int cellIndex;
List<CellChange> cellChanges;
public RowVisitor init(int cellIndex, List<CellChange> cellChanges) {
this.cellIndex = cellIndex;
this.cellChanges = cellChanges;
return this;
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
Cell newCell = new Cell();
newCell.value = cell.value;
newCell.recon = null;
CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell);
cellChanges.add(cellChange);
}
return false;
}
}.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges);
HistoryEntry historyEntry = new HistoryEntry(
project, "Discard recon results for " + columnName, massCellChange);
boolean done = project.processManager.queueProcess(
new QuickHistoryEntryProcess(project, historyEntry));
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
} catch (Exception e) {
respondException(response, e);
}
}
}

View File

@ -27,6 +27,15 @@ public class Recon implements Serializable, HasFields, Jsonizable {
public Judgment judgment = Judgment.None; public Judgment judgment = Judgment.None;
public ReconCandidate match = null; public ReconCandidate match = null;
public Recon dup() {
Recon r = new Recon();
r.features.putAll(new HashMap<String, Object>(features));
r.candidates.addAll(candidates);
r.judgment = judgment;
r.match = match;
return r;
}
@Override @Override
public Object getField(String name, Properties bindings) { public Object getField(String name, Properties bindings) {
if ("best".equals(name)) { if ("best".equals(name)) {
@ -75,15 +84,15 @@ public class Recon implements Serializable, HasFields, Jsonizable {
writer.key("j"); writer.key("j");
writer.value(judgmentToString()); writer.value(judgmentToString());
if (match != null) {
writer.key("m");
match.write(writer, options);
} else {
writer.key("c"); writer.array(); writer.key("c"); writer.array();
for (ReconCandidate c : candidates) { for (ReconCandidate c : candidates) {
c.write(writer, options); c.write(writer, options);
} }
writer.endArray(); writer.endArray();
if (match != null) {
writer.key("m");
match.write(writer, options);
} }
writer.endObject(); writer.endObject();

View File

@ -42,7 +42,7 @@ public class ReconCandidate implements Serializable, HasFields, Jsonizable {
writer.key("id"); writer.value(topicID); writer.key("id"); writer.value(topicID);
//writer.key("guid"); writer.value(topicGUID); //writer.key("guid"); writer.value(topicGUID);
writer.key("name"); writer.value(topicName); writer.key("name"); writer.value(topicName);
//writer.key("score"); writer.value(score); writer.key("score"); writer.value(score);
/* /*
writer.key("types"); writer.array(); writer.key("types"); writer.array();

View File

@ -5,6 +5,10 @@ function DataTableView(div) {
this._showRows(0); this._showRows(0);
} }
DataTableView.prototype.update = function(reset) {
this._showRows(reset ? 0 : theProject.rowModel.start);
};
DataTableView.prototype.render = function() { DataTableView.prototype.render = function() {
var self = this; var self = this;
var container = this._div.empty(); var container = this._div.empty();
@ -115,10 +119,23 @@ DataTableView.prototype.render = function() {
return; return;
} }
if (!("r" in cell) || cell.r == null) {
$(td).html(cell.v); $(td).html(cell.v);
} else {
if ("r" in cell && self._showRecon) { var r = cell.r;
var candidates = cell.r.c; if (r.j == "new") {
$(td).html(cell.v + " (new)");
} else if (r.j == "approve" && "m" in r && r.m != null) {
var match = cell.r.m;
$('<a></a>')
.attr("href", "http://www.freebase.com/view" + match.id)
.attr("target", "_blank")
.text(match.name)
.appendTo(td);
} else {
$(td).html(cell.v);
if (self._showRecon && "c" in r && r.c.length > 0) {
var candidates = r.c;
var ul = $('<ul></ul>').appendTo(td); var ul = $('<ul></ul>').appendTo(td);
for (var i = 0; i < candidates.length; i++) { for (var i = 0; i < candidates.length; i++) {
@ -129,6 +146,9 @@ DataTableView.prototype.render = function() {
.attr("target", "_blank") .attr("target", "_blank")
.text(candidate.name) .text(candidate.name)
.appendTo(li); .appendTo(li);
$('<span></span>').addClass("recon-score").text("(" + Math.round(candidate.score) + ")").appendTo(li);
}
}
} }
} }
}; };
@ -221,8 +241,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
{ {
label: "Filter", label: "Filter",
submenu: [ submenu: [
{ "heading" : "On Cell Content" },
{ {
label: "By Nominal Choices", label: "Simple Facet",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"list", "list",
@ -234,9 +255,18 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
); );
} }
}, },
{
label: "Custom Facet ...",
click: function() {
var expression = window.prompt("Enter expression", 'value');
if (expression != null) {
self._doFilterByExpression(column, expression);
}
}
},
{}, {},
{ {
label: "By Simple Text Search", label: "Text Search",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"text", "text",
@ -250,12 +280,12 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
} }
}, },
{ {
label: "By Regular Expression", label: "Regular Expression Search",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"text", "text",
{ {
"name" : column.headerLabel, "name" : column.headerLabel + " (regex)",
"cellIndex" : column.cellIndex, "cellIndex" : column.cellIndex,
"mode" : "regex", "mode" : "regex",
"caseSensitive" : true "caseSensitive" : true
@ -263,58 +293,28 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
); );
} }
}, },
{ "heading" : "By Reconciliation Features" },
{ {
label: "By Custom Expression ...", label: "By Judgment",
click: function() { click: function() {
var expression = window.prompt("Enter expression", 'value'); ui.browsingEngine.addFacet(
if (expression != null) { "list",
self._doFilterByExpression(column, expression); {
"name" : column.headerLabel + ": judgment",
"cellIndex" : column.cellIndex,
"expression" : "cell.recon.judgment"
} }
);
} }
}, },
{}, {},
{ {
label: "By Reconciliation Features", label: "Best Candidate's Relevance Score",
submenu: [
{
label: "By Type Match",
click: function() {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel + ": recon type match",
"cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.typeMatch"
},
{
"scroll" : false
}
);
}
},
{
label: "By Name Match",
click: function() {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel + ": recon name match",
"cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.nameMatch"
},
{
"scroll" : false
}
);
}
},
{
label: "By Best Candidate's Score",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"range", "range",
{ {
"name" : column.headerLabel + ": best candidate's score", "name" : column.headerLabel + ": best candidate's relevance score",
"cellIndex" : column.cellIndex, "cellIndex" : column.cellIndex,
"expression" : "cell.recon.best.score", "expression" : "cell.recon.best.score",
"mode" : "range", "mode" : "range",
@ -327,12 +327,45 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
} }
}, },
{ {
label: "By Name's Edit Distance", label: "Best Candidate's Type Match",
click: function() {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel + ": best candidate's type match",
"cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.typeMatch"
},
{
"scroll" : false
}
);
}
},
{
label: "Best Candidate's Name Match",
click: function() {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel + ": best candidate's name match",
"cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.nameMatch"
},
{
"scroll" : false
}
);
}
},
{},
{
label: "Best Candidate's Name Edit Distance",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"range", "range",
{ {
"name" : column.headerLabel + ": name's edit distance", "name" : column.headerLabel + ": best candidate's name edit distance",
"cellIndex" : column.cellIndex, "cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.nameLevenshtein", "expression" : "cell.recon.features.nameLevenshtein",
"mode" : "range", "mode" : "range",
@ -346,12 +379,12 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
} }
}, },
{ {
label: "By Name's Word Similarity", label: "Best Candidate's Name Word Similarity",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"range", "range",
{ {
"name" : column.headerLabel + ": name's word similarity", "name" : column.headerLabel + ": best candidate's name word similarity",
"cellIndex" : column.cellIndex, "cellIndex" : column.cellIndex,
"expression" : "cell.recon.features.nameWordDistance", "expression" : "cell.recon.features.nameWordDistance",
"mode" : "range", "mode" : "range",
@ -364,8 +397,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
); );
} }
}, },
{},
{ {
label: "By Best Candidate's Types", label: "Best Candidate's Types",
click: function() { click: function() {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"list", "list",
@ -378,8 +412,6 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
} }
} }
] ]
}
]
}, },
{ {
label: "Collapse/Expand", label: "Collapse/Expand",
@ -445,38 +477,39 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
label: "Reconcile", label: "Reconcile",
submenu: [ submenu: [
{ {
label: "Initiate on Filtered Rows...", label: "Start Reconciling ...",
tooltip: "Reconcile text in this column with topics on Freebase",
click: function() { click: function() {
new ReconDialog(index); new ReconDialog(index);
} }
}, },
{}, {},
{ {
label: "Approve Filtered Rows", label: "Approve Best Candidates",
click: function() {} tooltip: "Approve best reconciliaton candidate per cell in this column for all current filtered rows",
click: function() {
self._doApproveBestCandidates(column);
}
},
{
label: "Approve As New Topics",
tooltip: "Set to create new topics for cells in this column for all current filtered rows",
click: function() {
self._doApproveNewTopics(column);
}
},
{
label: "Discard Reconciliation Results",
tooltip: "Discard reconciliaton results in this column for all current filtered rows",
click: function() {
self._doDiscardReconResults(column);
}
} }
] ]
} }
], elmt); ], elmt);
}; };
DataTableView.prototype._doTextTransform = function(column, expression) {
var self = this;
$.post(
"/command/do-text-transform?" + $.param({ project: theProject.id, cell: column.cellIndex, expression: expression }),
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) {
if (data.code == "ok") {
self.update();
ui.historyWidget.update();
} else {
ui.processWidget.update();
}
},
"json"
);
};
DataTableView.prototype._doFilterByExpression = function(column, expression) { DataTableView.prototype._doFilterByExpression = function(column, expression) {
ui.browsingEngine.addFacet( ui.browsingEngine.addFacet(
"list", "list",
@ -488,7 +521,52 @@ DataTableView.prototype._doFilterByExpression = function(column, expression) {
); );
}; };
DataTableView.prototype.update = function(reset) { DataTableView.prototype._createUpdateFunction = function() {
this._showRows(reset ? 0 : theProject.rowModel.start); var self = this;
return function(data) {
if (data.code == "ok") {
self.update();
ui.historyWidget.update();
} else {
ui.processWidget.update();
}
};
}; };
DataTableView.prototype._doPostThenUpdate = function(command, params) {
params.project = theProject.id;
$.post(
"/command/" + command + "?" + $.param(params),
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) },
this._createUpdateFunction(),
"json"
);
};
DataTableView.prototype._doTextTransform = function(column, expression) {
this._doPostThenUpdate(
"do-text-transform",
{ cell: column.cellIndex, expression: expression }
);
};
DataTableView.prototype._doDiscardReconResults = function(column) {
this._doPostThenUpdate(
"discard-reconcile",
{ cell: column.cellIndex }
);
};
DataTableView.prototype._doApproveBestCandidates = function(column) {
this._doPostThenUpdate(
"approve-reconcile",
{ cell: column.cellIndex }
);
};
DataTableView.prototype._doApproveNewTopics = function(column) {
this._doPostThenUpdate(
"approve-new-reconcile",
{ cell: column.cellIndex }
);
};

View File

@ -78,7 +78,12 @@ MenuSystem.createAndShowStandardMenu = function(items, elmt, options) {
}); });
} else { } else {
menuItem.html(item.label).click(item.click); menuItem.html(item.label).click(item.click);
if ("tooltip" in item) {
menuItem.attr("title", item.tooltip);
} }
}
} else if ("heading" in item) {
$('<div></div>').addClass("menu-section").text(item.heading).appendTo(menu);
} else { } else {
$('<hr />').appendTo(menu); $('<hr />').appendTo(menu);
} }

View File

@ -86,3 +86,8 @@ a.facet-choice-link:hover {
.facet-text-body input { .facet-text-body input {
width: 98%; width: 98%;
} }
.recon-score {
color: #aaa;
margin-left: 0.5em;
}

View File

@ -59,7 +59,7 @@ a.inaction {
.menu-container { .menu-container {
position: absolute; position: absolute;
width: 200px; width: 250px;
background: white; background: white;
padding: 1px; padding: 1px;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -90,6 +90,13 @@ a.menu-item img {
border: none; border: none;
} }
.menu-section {
padding: 2px 7px;
background: #aaa;
color: white;
font-weight: bold;
}
.dialog-overlay { .dialog-overlay {
background: black; background: black;
opacity: 0.3; opacity: 0.3;