mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-03 20:02:47 +00:00 
			
		
		
		
	Add rudimentary rework of corpus analysis
This commit is contained in:
		@@ -20,7 +20,7 @@ class CorpusAnalysisClient {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadListeners() {
 | 
			
		||||
  loadSocketEventListeners() {
 | 
			
		||||
    for (let [type, listener] of Object.entries(this.socketEventListeners)) {
 | 
			
		||||
      listener(this);
 | 
			
		||||
    }
 | 
			
		||||
@@ -31,16 +31,6 @@ class CorpusAnalysisClient {
 | 
			
		||||
    this.displays[type] = corpusAnalysisDisplay;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // /**
 | 
			
		||||
  //  * Initializes the interactive corpus analysis session via socket.io.
 | 
			
		||||
  //  * This function uses helper functions.
 | 
			
		||||
  //  */
 | 
			
		||||
  // initSession() {
 | 
			
		||||
  //   let request = this.requestSession();
 | 
			
		||||
  //   let recvieveSession = this.recvieveSession();
 | 
			
		||||
  //   console.info('corpus_analysis_init: Client waiting for response');  // this happens inbetween the two functions above
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Requests a corpus analysis session via socket.io.
 | 
			
		||||
   * Opens a loading modal at the start of the request
 | 
			
		||||
@@ -56,17 +46,6 @@ class CorpusAnalysisClient {
 | 
			
		||||
    this.socket.emit('corpus_analysis_init', this.corpusId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // /**
 | 
			
		||||
  //  * Sends a query to the server and handles the response to that query.
 | 
			
		||||
  //  * This function uses helper functions.
 | 
			
		||||
  //  */
 | 
			
		||||
  // query(queryStr) {
 | 
			
		||||
  //   let requestQueryData = this.requestQueryData(queryStr);
 | 
			
		||||
  //   let recieveQueryProcessStatus = this.recieveQueryProcessStatus();
 | 
			
		||||
  //   let recieveQueryData = this.recieveQueryData();
 | 
			
		||||
  //   console.info('corpus_analysis_query: Client waiting for query data');  // this happens inbetween the two functions above
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sends the query string to the server.
 | 
			
		||||
   * Should be a private method if ES2020 is finalized (Maybe?)
 | 
			
		||||
@@ -165,5 +144,5 @@ class SocketEventListener {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// export both Classes from this module
 | 
			
		||||
// export Classes from this module
 | 
			
		||||
export {CorpusAnalysisClient, CorpusAnalysisDisplay, SocketEventListener};
 | 
			
		||||
							
								
								
									
										84
									
								
								web/app/static/js/modules/nopaque.listenerCallbacks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								web/app/static/js/modules/nopaque.listenerCallbacks.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This callback is called when the SocketEventListener 'recieveQueryData'
 | 
			
		||||
 * has been triggered. It takes the incoming chunk and renders the results in a
 | 
			
		||||
 * the results.jsList. It can eiterh hande llive incoming data or imported
 | 
			
		||||
 * results data.
 | 
			
		||||
 */
 | 
			
		||||
function queryRenderResults(payload, client, imported=false) {
 | 
			
		||||
  console.info("Current recieved chunk:", payload.chunk);
 | 
			
		||||
  if (payload.chunk.cpos_ranges == true) {
 | 
			
		||||
    client.results.data["cpos_ranges"] = true;
 | 
			
		||||
  } else {
 | 
			
		||||
    client.results.data["cpos_ranges"] = false;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * resultItem is a list where
 | 
			
		||||
   */
 | 
			
		||||
  let resultItems = [];
 | 
			
		||||
  // get infos for full match row
 | 
			
		||||
  for (let [index, match] of payload.chunk.matches.entries()) {
 | 
			
		||||
    resultItems.push({...match, ...{"index": index + client.results.data.matches.length}});
 | 
			
		||||
  }
 | 
			
		||||
  if (!imported) {
 | 
			
		||||
    // update progress bar
 | 
			
		||||
    // queryResultsDeterminateElement.style.width = `${payload.progress}%`;
 | 
			
		||||
    client.results.jsList.add(resultItems, (items) => {
 | 
			
		||||
      for (let item of items) {
 | 
			
		||||
        item.elm = client.results.jsList.createResultRowElement(item, payload.chunk);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    helperQueryRenderResults(payload, client);
 | 
			
		||||
    // if (progress === 100) {
 | 
			
		||||
    //   resultsCreateElement.classList.remove("disabled");
 | 
			
		||||
    //   queryResultsProgressElement.classList.add("hide");
 | 
			
		||||
    //   queryResultsUserFeedbackElement.classList.add("hide");
 | 
			
		||||
    //   resultsExportElement.classList.remove("disabled");
 | 
			
		||||
    //   addToSubResultsElement.removeAttribute("disabled");
 | 
			
		||||
    //   // inital expert mode check and sub results activation
 | 
			
		||||
    //   client.results.jsList.activateInspect();
 | 
			
		||||
    //   if (addToSubResultsElement.checked) {
 | 
			
		||||
    //     client.results.jsList.activateAddToSubResults();
 | 
			
		||||
    //   }
 | 
			
		||||
    //   if (expertModeSwitchElement.checked) {
 | 
			
		||||
    //     client.results.jsList.expertModeOn("query-display");
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
  } else if (imported) {
 | 
			
		||||
    client.results.jsList.add(resultItems, (items) => {
 | 
			
		||||
      for (let item of items) {
 | 
			
		||||
        item.elm = client.results.jsList.createResultRowElement(item, payload.chunk,
 | 
			
		||||
                                                         true);
 | 
			
		||||
      }
 | 
			
		||||
      helperQueryRenderResults(payload, client);
 | 
			
		||||
      progress = 100;
 | 
			
		||||
      client.results.jsList.activateInspect();
 | 
			
		||||
      if (expertModeSwitchElement.checked) {
 | 
			
		||||
        client.results.jsList.expertModeOn("query-display");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function helperQueryRenderResults (payload, client) {
 | 
			
		||||
  // updating table on finished item creation callback via createResultRowElement
 | 
			
		||||
  client.results.jsList.update();
 | 
			
		||||
  client.results.jsList.changeContext(); // sets lr context on first result load
 | 
			
		||||
  // incorporating new chunk results into full results
 | 
			
		||||
  client.results.data.matches.push(...payload.chunk.matches);
 | 
			
		||||
  client.results.data.addData(payload.chunk.cpos_lookup, "cpos_lookup");
 | 
			
		||||
  client.results.data.addData(payload.chunk.text_lookup, "text_lookup");
 | 
			
		||||
  // complete metaData
 | 
			
		||||
  // client.results.metaData.add();
 | 
			
		||||
  // show user current and total match count
 | 
			
		||||
  // receivedMatchCountElement.innerText = `${client.results.data.matches.length}`;
 | 
			
		||||
  // textLookupCountElement.innerText = `${Object.keys(client.results.data.text_lookup).length}`;
 | 
			
		||||
  let titles = new Array();
 | 
			
		||||
  for (let [key, value] of Object.entries(client.results.data.text_lookup)) {
 | 
			
		||||
    titles.push(`${value.title} (${value.publishing_year})`);
 | 
			
		||||
  };
 | 
			
		||||
  // textTitlesElement.innerText = `${titles.join(", ")}`;
 | 
			
		||||
  // upate progress status
 | 
			
		||||
  // progress = payload.progress;  // global declaration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { queryRenderResults }
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import {queryRenderResults} from './nopaque.listenerCallbacks.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Recieves a corpus analysis session via socket.io.
 | 
			
		||||
 * Closes the loading modal that has been opend with requestSession at the
 | 
			
		||||
@@ -34,11 +36,13 @@ function recieveSession(client) {
 | 
			
		||||
 * Recieves the query process status before any actual results are being
 | 
			
		||||
 * transmitted. So it recieves error codes if a query failed or
 | 
			
		||||
 * was invalid etc.
 | 
			
		||||
 * Also prepares the result.jsList for the incoming data.
 | 
			
		||||
 */
 | 
			
		||||
function recieveQueryStatus(client) {
 | 
			
		||||
  client.socket.on('corpus_analysis_query', (response) => {
 | 
			
		||||
    console.group('corpus_analysis_query: Client recieving query process',
 | 
			
		||||
                 'status via socket.on');
 | 
			
		||||
    client.results.clearAll();
 | 
			
		||||
    console.info(response);
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  });
 | 
			
		||||
@@ -51,10 +55,13 @@ function recieveQueryData(client) {
 | 
			
		||||
  client.socket.on('corpus_analysis_query_results', (response) => {
 | 
			
		||||
    console.group('corpus_analysis_query_results: Client recieving query',
 | 
			
		||||
                  'data via socket.on');
 | 
			
		||||
    console.info(response);
 | 
			
		||||
    console.info('Recieved chunk', response);
 | 
			
		||||
    queryRenderResults(response.payload, client);
 | 
			
		||||
    console.info('Added chunk data to results.data and rendered it with',
 | 
			
		||||
                 'results.jsList')
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// export listeners from this module
 | 
			
		||||
export {recieveSession, recieveQueryStatus, recieveQueryData};
 | 
			
		||||
export { recieveSession, recieveQueryStatus, recieveQueryData };
 | 
			
		||||
@@ -22,7 +22,7 @@ class Results {
 | 
			
		||||
class Data {
 | 
			
		||||
  // Sets empty object structure. Also usefull to delete old results.
 | 
			
		||||
  // matchCount default is 0
 | 
			
		||||
  init(matchCount = 0) {
 | 
			
		||||
  init(matchCount = 0, type = "results") {
 | 
			
		||||
    this["matches"] = [];  // list of all c with lc and rc
 | 
			
		||||
    this["cpos_lookup"] = {};  // object contains all this key value pair
 | 
			
		||||
    this["text_lookup"] = {};  // same as above for all text ids
 | 
			
		||||
@@ -122,4 +122,6 @@ class MetaData {
 | 
			
		||||
  init(json = {}) {
 | 
			
		||||
    Object.assign(this, json);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {Results, Data, MetaData};
 | 
			
		||||
@@ -425,15 +425,31 @@ RessourceList.options = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResultsList extends List {
 | 
			
		||||
  constructor(idOrElement, options={}) {
 | 
			
		||||
  super(idOrElement, options);
 | 
			
		||||
  this.eventTokens = {};  // all span tokens which are holdeing events if expert
 | 
			
		||||
  // mode is on. Collected here to delete later on
 | 
			
		||||
  this.currentExpertTokenElements = {}; // all token elements which have added
 | 
			
		||||
  // classes like chip and hoverable for expert view. Collected
 | 
			
		||||
  //here to delete later on
 | 
			
		||||
  this.addToSubResultsStatus = {};  // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons.
 | 
			
		||||
  this.addToSubResultsIdsToShow = new Set();  // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user.
 | 
			
		||||
  static options = {
 | 
			
		||||
    page: 10,
 | 
			
		||||
    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>`
 | 
			
		||||
  };
 | 
			
		||||
  constructor(idOrElement, options) {
 | 
			
		||||
    super(idOrElement, options);
 | 
			
		||||
    this.options = options;
 | 
			
		||||
    this.eventTokens = {};  // all span tokens which are holdeing events if expert
 | 
			
		||||
    // mode is on. Collected here to delete later on
 | 
			
		||||
    this.currentExpertTokenElements = {}; // all token elements which have added
 | 
			
		||||
    // classes like chip and hoverable for expert view. Collected
 | 
			
		||||
    //here to delete later on
 | 
			
		||||
    this.addToSubResultsStatus = {};  // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons.
 | 
			
		||||
    this.addToSubResultsIdsToShow = new Set();  // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  helperCreateCpos(cpos_ranges, cpos_values) {
 | 
			
		||||
@@ -479,12 +495,11 @@ class ResultsList extends List {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // get display options from display options form element
 | 
			
		||||
  static getDisplayOptions(displayOptionsFormElement) {
 | 
			
		||||
  static getDisplayOptions(htmlId) {
 | 
			
		||||
    // gets display options parameters
 | 
			
		||||
    let displayOptionsFormData
 | 
			
		||||
    let displayOptionsData;
 | 
			
		||||
    displayOptionsFormData = new FormData(displayOptionsFormElement);
 | 
			
		||||
    displayOptionsData =
 | 
			
		||||
    let displayOptionsFormElement = document.getElementById(htmlId);
 | 
			
		||||
    let displayOptionsFormData = new FormData(displayOptionsFormElement);
 | 
			
		||||
    let displayOptionsData =
 | 
			
		||||
      {
 | 
			
		||||
        "resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"),
 | 
			
		||||
        "resultsContex": displayOptionsFormData.get("display-options-form-result_context"),
 | 
			
		||||
@@ -1182,5 +1197,6 @@ class ResultsList extends List {
 | 
			
		||||
      `);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export {RessourceList, ResultsList};
 | 
			
		||||
 
 | 
			
		||||
@@ -75,25 +75,43 @@
 | 
			
		||||
          CorpusAnalysisDisplay,
 | 
			
		||||
          SocketEventListener} from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
 | 
			
		||||
  import {recieveSession, recieveQueryStatus,
 | 
			
		||||
          recieveQueryData} from '../../static/js/modules/nopaque.listenerFunctions.js'
 | 
			
		||||
          recieveQueryData} from '../../static/js/modules/nopaque.listenerFunctions.js';
 | 
			
		||||
  import {Results, Data, MetaData} from '../../static/js/nopaque.Results.js';
 | 
			
		||||
  import {ResultsList} from '../../static/js/nopaque.lists.js';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Second Phase:
 | 
			
		||||
   * Asynchronus and event driven code
 | 
			
		||||
   */
 | 
			
		||||
  document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
    // init some modals
 | 
			
		||||
    // Initialize the CorpusAnalysisClient
 | 
			
		||||
    const client = new CorpusAnalysisClient({{ corpus_id }}, nopaque.socket);
 | 
			
		||||
    console.info("CorpusAnalysisClient created as client:", client);
 | 
			
		||||
    // Initialize modals which are shown depending on events or client status
 | 
			
		||||
    const initLoadingElement = document.getElementById("init-display");
 | 
			
		||||
    const initLoadingModal = M.Modal.init(initLoadingElement,
 | 
			
		||||
                                          {"dismissible": false});
 | 
			
		||||
    // set up display elements
 | 
			
		||||
    // Set up display elements which hare show depending on the client status
 | 
			
		||||
    const initLoadingDisplay = new CorpusAnalysisDisplay(initLoadingModal);
 | 
			
		||||
    // set up CorpusAnalysisClient
 | 
			
		||||
    const client = new CorpusAnalysisClient({{ corpus_id }}, nopaque.socket);
 | 
			
		||||
    console.info("CorpusAnalysisClient created as client:", client);
 | 
			
		||||
    // register display elements to client
 | 
			
		||||
    // Register those display elements to client
 | 
			
		||||
    client.setDisplay("init", initLoadingDisplay);
 | 
			
		||||
    // register listeners and load them
 | 
			
		||||
    /**
 | 
			
		||||
     * Initializing the results object holding all the data of a query.
 | 
			
		||||
     * Also holds the metadata of one query.
 | 
			
		||||
     * resultsListOptions is set to determine how many results per apge are
 | 
			
		||||
     * shown etc.
 | 
			
		||||
     * Lastly it also contains the object ResultsList which is a list.js
 | 
			
		||||
     * subclass which handles the visual reprensetnation of the query data.
 | 
			
		||||
     */
 | 
			
		||||
     let displayOptionsData = ResultsList.getDisplayOptions('display-options-form');
 | 
			
		||||
     ResultsList.options.page = displayOptionsData["resultsPerPage"];
 | 
			
		||||
     let resultsList = new ResultsList("result-list", ResultsList.options);
 | 
			
		||||
     let resultsMetaData = new MetaData();
 | 
			
		||||
     let results = new Results(new Data(), resultsList, resultsMetaData);
 | 
			
		||||
     // make results part of the client
 | 
			
		||||
     client.results = results;
 | 
			
		||||
     console.info('Initialized the Results object.')
 | 
			
		||||
    // register listeners listening to socket.io events and load them
 | 
			
		||||
    const listenForSession = new SocketEventListener('corpus_analysis_init',
 | 
			
		||||
                                                    recieveSession);
 | 
			
		||||
    const listenForQueryStatus = new SocketEventListener('corpus_analysis_query',
 | 
			
		||||
@@ -102,10 +120,10 @@
 | 
			
		||||
                                                    recieveQueryData);
 | 
			
		||||
    client.setSocketEventListeners([listenForSession, listenForQueryStatus,
 | 
			
		||||
                                    listenForQueryData]);
 | 
			
		||||
    client.loadListeners();
 | 
			
		||||
    client.loadSocketEventListeners();
 | 
			
		||||
    // Session initialization
 | 
			
		||||
    client.requestSession();
 | 
			
		||||
    // send a query and recieve its answer data
 | 
			
		||||
    // Send a query and recieve its answer data
 | 
			
		||||
    let queryFormElement = document.getElementById("query-form");
 | 
			
		||||
    queryFormElement.addEventListener("submit", (event) => {
 | 
			
		||||
      try {
 | 
			
		||||
@@ -129,8 +147,8 @@
 | 
			
		||||
      // Prevent page from reloading on submit
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      // Get query string and send query to server
 | 
			
		||||
      // results.data.getQueryStr(queryFormElement);
 | 
			
		||||
      client.requestQueryData('"this" []* "that" within 10 words;');
 | 
			
		||||
      results.data.getQueryStr(queryFormElement);
 | 
			
		||||
      client.requestQueryData(results.data.query);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,9 @@
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/List.js/list.min.js') }}"></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/Socket.IO/socket.io.slim.js') }}"></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/nopaque.js') }}"></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/nopaque.lists.js') }}"></script>
 | 
			
		||||
    <script type="module">
 | 
			
		||||
      import {RessourceList} from '../../static/js/nopaque.lists.js'
 | 
			
		||||
    </script>
 | 
			
		||||
    <script>
 | 
			
		||||
      {% if current_user.is_authenticated %}
 | 
			
		||||
      {% if current_user.setting_dark_mode %}
 | 
			
		||||
@@ -240,43 +242,6 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
    <!--
 | 
			
		||||
    <footer class="page-footer">
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
          <div class="col s12 m3">
 | 
			
		||||
            <a href="https://www.dfg.de/">
 | 
			
		||||
              <img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col s12 m6">
 | 
			
		||||
            <h5 class="white-text" style="margin-left: -15px;">Contact</h5>
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <p>Contact our interdisciplinary team via email: <b>inf_sfb1288@lists.uni-bielefeld.de</b></p>
 | 
			
		||||
              <p>Silke Schwandt (Digital History), Johanna Vompras (Research Data Management), Julia Becker (Cultural Studies), Patrick Jentsch (Cognitive Informatics), Anna Neubert (Digital Humanities), Stephan Porada (Interdisciplinary Media Studies), Helene Schlicht (History, Philosophy)
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col s12 m3">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <div class="col s8">
 | 
			
		||||
                <h5 class="white-text">Reminder</h5>
 | 
			
		||||
                <p>
 | 
			
		||||
                  Check out our website for nopaque's upcomming release:<br>
 | 
			
		||||
                  <small>www.uni-bielefeld.de/sfb1288/projekte/inf.html</small>
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="col s4">
 | 
			
		||||
                <p> </p>
 | 
			
		||||
                <img class="responsive-img" src="{{ url_for('static', filename='images/qr_-_inf.svg') }}">
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
    -->
 | 
			
		||||
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/Materialize/materialize.min.js') }}"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user