mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-06-13 09:30:40 +00:00
Corpus analysis version 3.0 lul
This commit is contained in:
@ -4,34 +4,70 @@ class CorpusAnalysisClient {
|
||||
this.corpusId = corpusId;
|
||||
this.displays = {};
|
||||
this.socket = socket;
|
||||
this.resultList = {};
|
||||
|
||||
// js list options and intialization
|
||||
let displayOptionsData = this.getDisplayOptions(displayOptionsFormElement);
|
||||
let resultListOptions = {page: displayOptionsData["hitsPerPage"],
|
||||
pagination: [{
|
||||
name: "paginationTop",
|
||||
paginationClass: "paginationTop",
|
||||
innerWindow: 8,
|
||||
outerWindow: 1
|
||||
}, {
|
||||
paginationClass: "paginationBottom",
|
||||
innerWindow: 8,
|
||||
outerWindow: 1
|
||||
}],
|
||||
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
|
||||
item: `<span></span>`};
|
||||
this.resultList = new ResultList("result-list", resultListOptions);
|
||||
|
||||
// socket on event fpr corpous analysis initialization
|
||||
socket.on("pj_corpus_analysis_init", (response) => {
|
||||
var errorText;
|
||||
|
||||
if (response.code === 200) {
|
||||
console.log(`pj_corpus_analysis_init: ${response.code} - ${response.msg}`);
|
||||
if (this.callbacks.init != undefined) {this.callbacks.init(response.payload);}
|
||||
if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("success");}
|
||||
if (this.callbacks.init != undefined) {
|
||||
this.callbacks.init(response.payload);
|
||||
}
|
||||
if (this.displays.init != undefined) {
|
||||
this.displays.init.setVisibilityByStatus("success");
|
||||
}
|
||||
} else {
|
||||
errorText = `Error ${response.code} - ${response.msg}`;
|
||||
if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;}
|
||||
if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("error");}
|
||||
if (this.displays.init.errorContainer != undefined) {
|
||||
this.displays.init.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;
|
||||
}
|
||||
if (this.displays.init != undefined) {
|
||||
this.displays.init.setVisibilityByStatus("error");
|
||||
}
|
||||
console.error(`pj_corpus_analysis_init: ${errorText}`);
|
||||
}
|
||||
});
|
||||
|
||||
// socket on event for recieveing query results
|
||||
socket.on("pj_corpus_analysis_query", (response) => {
|
||||
var errorText;
|
||||
|
||||
if (response.code === 200) {
|
||||
console.log(`pj_corpus_analysis_query: ${response.code} - ${response.msg}`);
|
||||
if (this.callbacks.query != undefined) {this.callbacks.query(response.payload);}
|
||||
if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("success");}
|
||||
if (this.callbacks.query != undefined) {
|
||||
this.callbacks.query(response.payload);
|
||||
}
|
||||
if (this.displays.query != undefined) {
|
||||
this.displays.query.setVisibilityByStatus("success");
|
||||
}
|
||||
} else {
|
||||
errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
|
||||
nopaque.flash("error", errorText);
|
||||
if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;}
|
||||
if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("error");}
|
||||
if (this.displays.query.errorContainer != undefined) {
|
||||
this.displays.query.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;
|
||||
}
|
||||
if (this.displays.query != undefined) {
|
||||
this.displays.query.setVisibilityByStatus("error");
|
||||
}
|
||||
console.error(`pj_corpus_analysis_query: ${errorText}`);
|
||||
}
|
||||
});
|
||||
@ -42,14 +78,26 @@ class CorpusAnalysisClient {
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML == "";}
|
||||
if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("waiting");}
|
||||
if (this.displays.init.errorContainer != undefined) {
|
||||
this.displays.init.errorContainer.innerHTML == "";
|
||||
}
|
||||
if (this.displays.init != undefined) {
|
||||
this.displays.init.setVisibilityByStatus("waiting");
|
||||
}
|
||||
this.socket.emit("pj_corpus_analysis_init", this.corpusId);
|
||||
}
|
||||
|
||||
query(queryStr) {
|
||||
if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML == "";}
|
||||
if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("waiting");}
|
||||
sendQuery(queryStr) {
|
||||
let displayOptionsData;
|
||||
let resultListOptions;
|
||||
|
||||
if (this.displays.query.errorContainer != undefined) {
|
||||
this.displays.query.errorContainer.innerHTML == "";
|
||||
}
|
||||
if (this.displays.query != undefined) {
|
||||
this.displays.query.setVisibilityByStatus("waiting");
|
||||
}
|
||||
this.resultList.clear(); // empty list for new query
|
||||
nopaque.socket.emit("pj_corpus_analysis_query", queryStr);
|
||||
}
|
||||
|
||||
@ -62,6 +110,18 @@ class CorpusAnalysisClient {
|
||||
return queryStr
|
||||
}
|
||||
|
||||
getDisplayOptions(displayOptionsFormElement) {
|
||||
// gets display options parameters
|
||||
let displayOptionsFormData
|
||||
let displayOptionsData;
|
||||
displayOptionsFormData = new FormData(displayOptionsFormElement);
|
||||
displayOptionsData = {"resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"),
|
||||
"resultsContex": displayOptionsFormData.get("display-options-form-result_context"),
|
||||
"expertMode": displayOptionsFormData.get("display-options-form-expert_mode")};
|
||||
console.log(displayOptionsData);
|
||||
return displayOptionsData
|
||||
}
|
||||
|
||||
setCallback(type, callback) {
|
||||
// saves callback functions into an object. Key is function type, callback
|
||||
// is the callback function
|
||||
@ -81,30 +141,56 @@ class CorpusAnalysisDisplay {
|
||||
this.showOnError = element.querySelectorAll(".show-on-error");
|
||||
this.showOnSuccess = element.querySelectorAll(".show-on-success");
|
||||
this.showWhileWaiting = element.querySelectorAll(".show-while-waiting");
|
||||
this.hideOnComplete = element.querySelectorAll(".hide-on-complete")
|
||||
}
|
||||
|
||||
setVisibilityByStatus(status) {
|
||||
switch (status) {
|
||||
case "error":
|
||||
for (let element of this.showOnError) {element.classList.remove("hide");}
|
||||
for (let element of this.showOnSuccess) {element.classList.add("hide");}
|
||||
for (let element of this.showWhileWaiting) {element.classList.add("hide");}
|
||||
for (let element of this.showOnError) {
|
||||
element.classList.remove("hide");
|
||||
}
|
||||
for (let element of this.showOnSuccess) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of this.showWhileWaiting) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
break;
|
||||
case "success":
|
||||
for (let element of this.showOnError) {element.classList.add("hide");}
|
||||
for (let element of this.showOnSuccess) {element.classList.remove("hide");}
|
||||
for (let element of this.showWhileWaiting) {element.classList.add("hide");}
|
||||
for (let element of this.showOnError) {
|
||||
element.classList.add("hide");
|
||||
|
||||
}
|
||||
for (let element of this.showOnSuccess) {
|
||||
element.classList.remove("hide");
|
||||
}
|
||||
for (let element of this.showWhileWaiting) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
break;
|
||||
case "waiting":
|
||||
for (let element of this.showOnError) {element.classList.add("hide");}
|
||||
for (let element of this.showOnSuccess) {element.classList.add("hide");}
|
||||
for (let element of this.showWhileWaiting) {element.classList.remove("hide");}
|
||||
for (let element of this.showOnError) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of this.showOnSuccess) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of this.showWhileWaiting) {
|
||||
element.classList.remove("hide");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Hide all
|
||||
for (let element of this.showOnError) {element.classList.add("hide");}
|
||||
for (let element of this.showOnSuccess) {element.classList.add("hide");}
|
||||
for (let element of this.showWhileWaiting) {element.classList.add("hide");}
|
||||
for (let element of this.showOnError) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of this.showOnSuccess) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of this.showWhileWaiting) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ function helperSendQuery(queryData) {
|
||||
outerWindow: 1
|
||||
}],
|
||||
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
|
||||
item: `<span class="hidden"></span>`};
|
||||
item: `<span></span>`};
|
||||
resultList = new ResultList('result-list', resultListOptions);
|
||||
resultList.clear(); // empty list for new query
|
||||
}
|
||||
|
@ -1,32 +1,55 @@
|
||||
function querySetup(payload) {
|
||||
// This is called when a query was successfull
|
||||
console.log("Query setup.");
|
||||
console.log(payload);
|
||||
// some hiding
|
||||
queryResultsExportElement.classList.add("disabled");
|
||||
|
||||
console.log("Query initial setup seccessfull.");
|
||||
queryResultsDeterminateElement.style.width = "0%";
|
||||
queryResultsProgressElement.classList.remove("hide");
|
||||
receivedMatchNumElement.innerText = "0";
|
||||
textLookupNumElement.innerText = "0";
|
||||
matchNumElement.innerText = payload.num_matches;
|
||||
queryResultsUserFeedbackElement.classList.remove("hide");
|
||||
receivedMatchCountElement.innerText = "0";
|
||||
textLookupCountElement.innerText = "0";
|
||||
matchCountElement.innerText = payload.match_count;
|
||||
// always re initializes results to delete old results from it
|
||||
results = {};
|
||||
results["matches"] = []; // list of all c with lc and rc
|
||||
results["cpos_lookup"] = {}; // object contains all cpos as key value pair
|
||||
results["text_lookup"] = {}; // same as above for all text ids
|
||||
results["num_matches"] = payload.num_matches;
|
||||
results["match_count"] = payload.match_count;
|
||||
results["query"] = client.getQueryStr(queryFormElement);
|
||||
}
|
||||
|
||||
function queryRenderResults(payload) {
|
||||
// This is called when results are transmitted.
|
||||
console.log("CHUNK:", payload.chunk);
|
||||
console.log("RESULTS:", results);
|
||||
// This is called when results are transmitted and being recieved
|
||||
console.log("Current recieved chunk:", payload.chunk);
|
||||
// upate progress status
|
||||
if (payload.progress === 100) {
|
||||
queryResultsProgressElement.classList.add("hide");
|
||||
queryResultsUserFeedbackElement.classList.add("hide");
|
||||
queryResultsExportElement.classList.remove("disabled");
|
||||
activateInspect();
|
||||
}
|
||||
// update progress bar
|
||||
queryResultsDeterminateElement.style.width = `${payload.progress}%`;
|
||||
results.matches.push(...payload.chunk.matches);
|
||||
receivedMatchNumElement.innerText = `${results.matches.length}`;
|
||||
// building the result list js list from incoming chunk
|
||||
resultItems = []; // list for holding every row item
|
||||
// get infos for full match row
|
||||
for (let [index, match] of payload.chunk.matches.entries()) {
|
||||
resultItems.push({...match, ...{"index": index + results.matches.length}});
|
||||
}
|
||||
client.resultList.add(resultItems, (items) => {
|
||||
for (let item of items) {
|
||||
item.elm = client.resultList.createResultRowElement(item, payload.chunk);
|
||||
}
|
||||
client.resultList.update();
|
||||
changeContext(); // sets lr context on first result load
|
||||
});
|
||||
// incorporating new chunk results into full results
|
||||
results.matches.push(...payload.chunk.matches);
|
||||
Object.assign(results.cpos_lookup, payload.chunk.cpos_lookup);
|
||||
Object.assign(results.text_lookup, payload.chunk.text_lookup);
|
||||
|
||||
textLookupNumElement.innerText = `${Object.keys(results.text_lookup).length}`;
|
||||
// show user current and total match count
|
||||
receivedMatchCountElement.innerText = `${results.matches.length}`;
|
||||
textLookupCountElement.innerText = `${Object.keys(results.text_lookup).length}`;
|
||||
console.log("Results recieved:", results);
|
||||
}
|
@ -149,7 +149,7 @@ class ResultList extends List {
|
||||
token = chunk["cpos_lookup"][cpos];
|
||||
hitCellElement.insertAdjacentHTML("beforeend", `<span class="token" data-cpos="${cpos}">${token["word"]} </span>`);
|
||||
// get text titles of every hit cpos token
|
||||
textTitles.add(chunk["text_lookup"][token["text"]]["title"]);
|
||||
textTitles.add(chunk["text_lookup"][token["text"]]["text_title"]);
|
||||
// add button to trigger more context to every match td
|
||||
var inspectBtn = document.createElement("a");
|
||||
inspectBtn.setAttribute("class", "btn-floating btn-flat waves-effect waves-light grey right inspect disabled");
|
||||
|
231
app/static/js/nopaque.pj_analyse_corpus.js
Normal file
231
app/static/js/nopaque.pj_analyse_corpus.js
Normal file
@ -0,0 +1,231 @@
|
||||
// ###### Download results functions ######
|
||||
// TODO: Maybe write these as class functions? For this maybe create a result class
|
||||
|
||||
// function creates a unique and safe filename for the download
|
||||
function createDownloadFilename() {
|
||||
let today;
|
||||
let currentDate;
|
||||
let currentTime;
|
||||
let safeFilename;
|
||||
let resultFilename;
|
||||
// get and create metadata
|
||||
console.log("Create Metadata!");
|
||||
today = new Date();
|
||||
currentDate = today.getUTCFullYear() + '-' + (today.getUTCMonth() +1) + '-' + today.getUTCDate();
|
||||
currentTime = today.getUTCHours() + ":" + today.getUTCMinutes() + ":" + today.getUTCSeconds();
|
||||
safeFilename = results["query"].replace(/[^a-z0-9_-]/gi, "_");
|
||||
resultFilename = "UTC-" + currentDate + "_" + currentTime + "_" + safeFilename;
|
||||
return resultFilename
|
||||
}
|
||||
|
||||
// function to download the results as JSON
|
||||
function downloadJSONRessource(resultFilename) {
|
||||
let dataStr;
|
||||
let downloadElement;
|
||||
// stringify JSON object for json download
|
||||
dataStr = JSON.stringify(results, undefined, "\t"); // use tabs to save some space
|
||||
// get downloadResultsElement
|
||||
downloadElement = document.getElementById("download-results-json");
|
||||
// start actual download
|
||||
download(downloadElement, dataStr, resultFilename, "text/json", ".json")
|
||||
}
|
||||
|
||||
// Function to download data as a Blob created from a string, should be multi purpose
|
||||
function download(downloadElem, dataStr, filename, type, filenameSlug) {
|
||||
let file;
|
||||
console.log("Start Download!");
|
||||
filename += filenameSlug;
|
||||
file = new Blob([dataStr], {type: type});
|
||||
if (window.navigator.msSaveOrOpenBlob) // IE10+
|
||||
window.navigator.msSaveOrOpenBlob(file, filename);
|
||||
else { // Others
|
||||
var url = URL.createObjectURL(file);
|
||||
downloadElem.href = url;
|
||||
downloadElem.download = filename;
|
||||
}
|
||||
}
|
||||
|
||||
// ###### Functions to inspect one match, to show more details ######
|
||||
|
||||
// activate inspect buttons if queryFinished is true
|
||||
function activateInspect(progress) {
|
||||
let inspectBtnElements;
|
||||
inspectBtnElements = document.getElementsByClassName("inspect");
|
||||
for (let inspectBtn of inspectBtnElements) {
|
||||
inspectBtn.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
//gets result cpos infos for one dataIndex to send back to the server
|
||||
function inspect(dataIndex) {
|
||||
console.log("Inspect!");
|
||||
console.log(result["matches"][dataIndex]["c"]);
|
||||
contextModal.open();
|
||||
nopaque.socket.emit("inspect_match", {"cpos": result["matches"][dataIndex]["c"]});
|
||||
}
|
||||
|
||||
function showMatchContext(message) {
|
||||
let contextResultsElement;
|
||||
let sentenceElement
|
||||
let token;
|
||||
let tokenElement;
|
||||
console.log("###### match_context ######");
|
||||
console.log("Incoming data:", message);
|
||||
contextResultsElement = document.getElementById("context-results");
|
||||
contextResultsElement.innerHTML = "<p> </p>";
|
||||
document.getElementById("context-modal-loading").classList.add("hide");
|
||||
document.getElementById("context-modal-ready").classList.remove("hide");
|
||||
|
||||
for (let [key, value] of Object.entries(message['context_s_cpos'])) {
|
||||
sentenceElement = document.createElement("p");
|
||||
for (let cpos of value) {
|
||||
token = message["cpos_lookup"][cpos];
|
||||
tokenElement = document.createElement("span");
|
||||
tokenElement.classList.add("token");
|
||||
if (message["match_cpos_list"].includes(cpos)) {
|
||||
tokenElement.classList.add("bold");
|
||||
tokenElement.classList.add("light-green");
|
||||
}
|
||||
tokenElement.dataset.cpos = cpos;
|
||||
tokenElement.innerText = token["word"];
|
||||
var expertModeSwitchElement = document.getElementById("expert-mode-switch");
|
||||
if (expertModeSwitchElement.checked) {
|
||||
expertModeOn([tokenElement], message);
|
||||
}
|
||||
sentenceElement.append(tokenElement);
|
||||
sentenceElement.append(document.createTextNode(" "));
|
||||
}
|
||||
contextResultsElement.append(sentenceElement);
|
||||
}
|
||||
}
|
||||
|
||||
// ###### Display options changing live how the matches are being displayed ######
|
||||
|
||||
// Event function that changes the shown hits per page.
|
||||
// Just alters the resultList.page property
|
||||
function changeHitsPerPage(event) {
|
||||
try {
|
||||
resultList.page = event.target.value;
|
||||
resultList.update();
|
||||
nopaque.flash("Updated matches per page.")
|
||||
} catch (e) {
|
||||
console.log("resultList has no results right now. Live update of items per page is useless for now.");
|
||||
}
|
||||
}
|
||||
|
||||
// Event function triggered on context select change and also if pagination is clicked
|
||||
function changeContext(event) {
|
||||
let newContextValue;
|
||||
let lc;
|
||||
let rc;
|
||||
let array;
|
||||
try {
|
||||
if (event.type === "change") {
|
||||
nopaque.flash("Updated context per match!");
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log(e);
|
||||
// console.log("This error is expected.");
|
||||
} finally {
|
||||
newContextValue = document.getElementById("display-options-form-result_context").value;
|
||||
console.log("Context value is:", newContextValue);
|
||||
lc = document.getElementsByClassName("left-context");
|
||||
rc = document.getElementsByClassName("right-context");
|
||||
for (let element of lc) {
|
||||
array = Array.from(element.childNodes);
|
||||
for (let element of array.slice(newContextValue)) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of array.slice(0, newContextValue)) {
|
||||
element.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
for (let element of rc) {
|
||||
array = Array.from(element.childNodes);
|
||||
for (let element of array.slice(newContextValue)) {
|
||||
element.classList.add("hide");
|
||||
}
|
||||
for (let element of array.slice(0, newContextValue)) {
|
||||
element.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ###### Expert view event functions ######
|
||||
|
||||
// Event function to check if pagination is used and then look if
|
||||
// expertModeSwitchElement is checked
|
||||
// if checked than expertModeOn is executed
|
||||
// if unchecked expertModeOff is executed
|
||||
function eventHandlerCheck(event) {
|
||||
console.log("pagination used!");
|
||||
console.log(expertModeSwitchElement.checked);
|
||||
if (expertModeSwitchElement.checked) {
|
||||
expertModeOn(event.currentTarget.tokenElements, result);
|
||||
} else if (!expertModeSwitchElement.checked) {
|
||||
event.preventDefault();
|
||||
console.log("prevented! Destroy");
|
||||
expertModeOff(event.currentTarget.tokenElements);
|
||||
}
|
||||
}
|
||||
|
||||
// function to apply extra information and animation to every token
|
||||
function expertModeOn(tokenElements, result_lookup) {
|
||||
let token;
|
||||
|
||||
console.log("expertModeOn!");
|
||||
for (let tokenElement of tokenElements) {
|
||||
tokenElement.classList.add("chip");
|
||||
tokenElement.classList.add("hoverable");
|
||||
tokenElement.classList.add("expert-view");
|
||||
token = result_lookup["cpos_lookup"][tokenElement.dataset.cpos];
|
||||
tokenElement.addEventListener("mouseover", function(event) {
|
||||
console.log("Mouseover!");
|
||||
console.log(event.target);
|
||||
token = result_lookup["cpos_lookup"][event.target.dataset.cpos];
|
||||
addToolTipToTokenElement(event.target, token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fuction that creates Tooltip for one token and extracts the corresponding
|
||||
// infos from the result JSON
|
||||
function addToolTipToTokenElement(tokenElement, token) {
|
||||
M.Tooltip.init(tokenElement,
|
||||
{"html": `<table>
|
||||
<tr>
|
||||
<th>Token information</th>
|
||||
<th>Source information</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left-align">
|
||||
Word: ${token["word"]}<br>
|
||||
Lemma: ${token["lemma"]}<br>
|
||||
POS: ${token["pos"]}<br>
|
||||
Simple POS: ${token["simple_pos"]}<br>
|
||||
NER: ${token["ner"]}
|
||||
</td>
|
||||
<td class="left-align">
|
||||
Title: ${result["text_lookup"][token["text"]]["title"]}<br>
|
||||
Author: ${result["text_lookup"][token["text"]]["author"]}<br>
|
||||
Publishing year: ${result["text_lookup"][token["text"]]["publishing_year"]}
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
"inDuration": 1500,
|
||||
"margin": 15,
|
||||
"position": "top",
|
||||
"transitionMovement": 0});
|
||||
}
|
||||
|
||||
// function to remove extra informations and animations from tokens
|
||||
function expertModeOff(tokenElements) {
|
||||
console.log("expertModeOff!");
|
||||
for (let tokenElement of tokenElements) {
|
||||
tokenElement.classList.remove("chip");
|
||||
tokenElement.classList.remove("hoverable");
|
||||
tokenElement.classList.remove("expert-view");
|
||||
tokenElement.outerHTML = tokenElement.outerHTML; // this is actually a workaround, but it works pretty fast
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user