mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-06-27 08:20:34 +00:00
Add a full featured cqi Javascript client for cqi_over_sio
This commit is contained in:
289
app/static/js/cqi/models/attributes.js
Normal file
289
app/static/js/cqi/models/attributes.js
Normal file
@ -0,0 +1,289 @@
|
||||
cqi.models.attributes = {};
|
||||
|
||||
|
||||
cqi.models.attributes.Attribute = class Attribute extends cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.cl_drop_attribute(this.apiName);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.AttributeCollection = class AttributeCollection extends cqi.models.resource.Collection {
|
||||
/** @type{typeof cqi.models.attributes.Attribute} */
|
||||
static model = cqi.models.attributes.Attribute;
|
||||
|
||||
/**
|
||||
* @param {cqi.CQiClient} client
|
||||
* @param {cqi.models.corpora.Corpus} corpus
|
||||
*/
|
||||
constructor(client, corpus) {
|
||||
super(client);
|
||||
/** @type {cqi.models.corpora.Corpus} */
|
||||
this.corpus = corpus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} attributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(attributeName) {
|
||||
/** @type{string} */
|
||||
let apiName = `${this.corpus.apiName}.${attributeName}`;
|
||||
return {
|
||||
api_name: apiName,
|
||||
name: attributeName,
|
||||
size: await this.client.api.cl_attribute_size(apiName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} attributeName
|
||||
* @returns {Promise<cqi.models.attributes.Attribute>}
|
||||
*/
|
||||
async get(attributeName) {
|
||||
return this.prepareModel(await this._get(attributeName));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.AlignmentAttribute = class AlignmentAttribute extends cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<[number, number, number, number]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_alg2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2alg(this.apiName, cposList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.AlignmentAttributeCollection = class AlignmentAttributeCollection extends cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof cqi.models.attributes.AlignmentAttribute} */
|
||||
static model = cqi.models.attributes.AlignmentAttribute;
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.models.attributes.AlignmentAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let alignmentAttributeNames = await this.client.api.corpus_alignment_attributes(this.corpus.apiName);
|
||||
/** @type {cqi.models.attributes.AlignmentAttribute[]} */
|
||||
let alignmentAttributes = [];
|
||||
for (let alignmentAttributeName of alignmentAttributeNames) {
|
||||
alignmentAttributes.push(await this.get(alignmentAttributeName));
|
||||
}
|
||||
return alignmentAttributes;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.PositionalAttribute = class PositionalAttribute extends cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get lexiconSize() {
|
||||
return this.attrs.lexicon_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_id2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cposByIds(idList) {
|
||||
return await this.client.api.cl_idlist2cpos(this.apiName, idList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async freqsByIds(idList) {
|
||||
return await this.client.api.cl_id2freq(this.apiName, idList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2id(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} regex
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByRegex(regex) {
|
||||
return await this.client.api.cl_regex2id(this.apiName, regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} valueList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByValues(valueList) {
|
||||
return await this.client.api.cl_str2id(this.apiName, valueList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2str(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByIds(idList) {
|
||||
return await this.client.api.cl_id2str(this.apiName, idList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.PositionalAttributeCollection = class PositionalAttributeCollection extends cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof cqi.models.attributes.PositionalAttribute} */
|
||||
static model = cqi.models.attributes.PositionalAttribute;
|
||||
|
||||
/**
|
||||
* @param {string} positionalAttributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(positionalAttributeName) {
|
||||
let positionalAttribute = await super._get(positionalAttributeName);
|
||||
positionalAttribute.lexicon_size = await this.client.api.cl_lexicon_size(positionalAttribute.api_name);
|
||||
return positionalAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.models.attributes.PositionalAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
let positionalAttributeNames = await this.client.api.corpus_positional_attributes(this.corpus.apiName);
|
||||
let positionalAttributes = [];
|
||||
for (let positionalAttributeName of positionalAttributeNames) {
|
||||
positionalAttributes.push(await this.get(positionalAttributeName));
|
||||
}
|
||||
return positionalAttributes;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.StructuralAttribute = class StructuralAttribute extends cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasValues() {
|
||||
return this.attrs.has_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<[number, number]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_struc2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2struc(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async lboundByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2lbound(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async rboundByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2rbound(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByIds(idList) {
|
||||
return await this.client.api.cl_struc2str(this.apiName, idList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.attributes.StructuralAttributeCollection = class StructuralAttributeCollection extends cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof cqi.models.attributes.StructuralAttribute} */
|
||||
static model = cqi.models.attributes.StructuralAttribute;
|
||||
|
||||
/**
|
||||
* @param {string} structuralAttributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(structuralAttributeName) {
|
||||
let structuralAttribute = await super._get(structuralAttributeName);
|
||||
structuralAttribute.has_values = await this.client.api.cl_has_values(structuralAttribute.api_name);
|
||||
return structuralAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.models.attributes.StructuralAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
let structuralAttributeNames = await this.client.api.corpus_structural_attributes(this.corpus.apiName);
|
||||
let structuralAttributes = [];
|
||||
for (let structuralAttributeName of structuralAttributeNames) {
|
||||
structuralAttributes.push(await this.get(structuralAttributeName));
|
||||
}
|
||||
return structuralAttributes;
|
||||
}
|
||||
};
|
127
app/static/js/cqi/models/corpora.js
Normal file
127
app/static/js/cqi/models/corpora.js
Normal file
@ -0,0 +1,127 @@
|
||||
cqi.models.corpora = {};
|
||||
|
||||
|
||||
cqi.models.corpora.Corpus = class Corpus extends cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get charset() {
|
||||
return this.attrs.charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
get properties() {
|
||||
return this.attrs?.properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {cqi.models.attributes.AlignmentAttributeCollection}
|
||||
*/
|
||||
get alignment_attributes() {
|
||||
return new cqi.models.attributes.AlignmentAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {cqi.models.attributes.PositionalAttributeCollection}
|
||||
*/
|
||||
get positional_attributes() {
|
||||
return new cqi.models.attributes.PositionalAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {cqi.models.attributes.StructuralAttributeCollection}
|
||||
*/
|
||||
get structural_attributes() {
|
||||
return new cqi.models.attributes.StructuralAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {cqi.models.subcorpora.SubcorpusCollection}
|
||||
*/
|
||||
get subcorpora() {
|
||||
return new cqi.models.subcorpora.SubcorpusCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.corpus_drop_corpus(this.apiName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @param {string} query
|
||||
* @returns {Promise<cqi.status.StatusOk>}
|
||||
*/
|
||||
async query(subcorpusName, query) {
|
||||
return await this.client.api.cqp_query(this.apiName, subcorpusName, query);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.corpora.CorpusCollection = class CorpusCollection extends cqi.models.resource.Collection {
|
||||
/** @type {typeof cqi.models.corpora.Corpus} */
|
||||
static model = cqi.models.corpora.Corpus;
|
||||
|
||||
/**
|
||||
* @param {string} corpusName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(corpusName) {
|
||||
return {
|
||||
api_name: corpusName,
|
||||
charset: await this.client.api.corpus_charset(corpusName),
|
||||
// full_name: await this.client.api.corpus_full_name(api_name),
|
||||
// info: await this.client.api.corpus_info(api_name),
|
||||
name: corpusName,
|
||||
properties: await this.client.api.corpus_properties(corpusName),
|
||||
size: await this.client.api.cl_attribute_size(`${corpusName}.word`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpusName
|
||||
* @returns {Promise<cqi.models.corpora.Corpus>}
|
||||
*/
|
||||
async get(corpusName) {
|
||||
return this.prepareModel(await this._get(corpusName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.models.corpora.Corpus[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let corpusNames = await this.client.api.corpus_list_corpora();
|
||||
/** @type {cqi.models.corpora.Corpus[]} */
|
||||
let corpora = [];
|
||||
for (let corpusName of corpusNames) {
|
||||
corpora.push(await this.get(corpusName));
|
||||
}
|
||||
return corpora;
|
||||
}
|
||||
};
|
1
app/static/js/cqi/models/package.js
Normal file
1
app/static/js/cqi/models/package.js
Normal file
@ -0,0 +1 @@
|
||||
cqi.models = {};
|
90
app/static/js/cqi/models/resource.js
Normal file
90
app/static/js/cqi/models/resource.js
Normal file
@ -0,0 +1,90 @@
|
||||
cqi.models.resource = {};
|
||||
|
||||
|
||||
/**
|
||||
* A base class for representing a single object on the server.
|
||||
*/
|
||||
cqi.models.resource.Model = class Model {
|
||||
/**
|
||||
* @param {object} attrs
|
||||
* @param {cqi.CQiClient} client
|
||||
* @param {cqi.models.resource.Collection} collection
|
||||
*/
|
||||
constructor(attrs, client, collection) {
|
||||
/**
|
||||
* A client pointing at the server that this object is on.
|
||||
*
|
||||
* @type {cqi.CQiClient}
|
||||
*/
|
||||
this.client = client;
|
||||
/**
|
||||
* The collection that this model is part of.
|
||||
*
|
||||
* @type {cqi.models.resource.Collection}
|
||||
*/
|
||||
this.collection = collection;
|
||||
/**
|
||||
* The raw representation of this object from the API
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
this.attrs = attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reload() {
|
||||
this.attrs = await this.collection.get(this.apiName).attrs;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A base class for representing all objects of a particular type on the server.
|
||||
*/
|
||||
cqi.models.resource.Collection = class Collection {
|
||||
/**
|
||||
* The type of object this collection represents, set by subclasses
|
||||
*
|
||||
* @type {typeof cqi.models.resource.Model}
|
||||
*/
|
||||
static model;
|
||||
|
||||
/**
|
||||
* @param {cqi.CQiClient} client
|
||||
*/
|
||||
constructor(client) {
|
||||
/**
|
||||
* A client pointing at the server that this object is on.
|
||||
*
|
||||
* @type {cqi.CQiClient}
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async list() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async get() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model from a set of attributes.
|
||||
*
|
||||
* @param {object} attrs
|
||||
* @returns {cqi.models.resource.Model}
|
||||
*/
|
||||
prepareModel(attrs) {
|
||||
return new this.constructor.model(attrs, this.client, this);
|
||||
}
|
||||
};
|
155
app/static/js/cqi/models/subcorpora.js
Normal file
155
app/static/js/cqi/models/subcorpora.js
Normal file
@ -0,0 +1,155 @@
|
||||
cqi.models.subcorpora = {};
|
||||
|
||||
|
||||
cqi.models.subcorpora.Subcorpus = class Subcorpus extends cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object}
|
||||
*/
|
||||
get fields() {
|
||||
return this.attrs.fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.cqp_drop_subcorpus(this.apiName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} field
|
||||
* @param {number} first
|
||||
* @param {number} last
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async dump(field, first, last) {
|
||||
return await this.client.api.cqp_dump_subcorpus(
|
||||
this.apiName,
|
||||
field,
|
||||
first,
|
||||
last
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} cutoff
|
||||
* @param {number} field
|
||||
* @param {cqi.models.attributes.PositionalAttribute} attribute
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async fdist1(cutoff, field, attribute) {
|
||||
return await this.client.api.cqp_fdist_1(
|
||||
this.apiName,
|
||||
cutoff,
|
||||
field,
|
||||
attribute.apiName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} cutoff
|
||||
* @param {number} field1
|
||||
* @param {cqi.models.attributes.PositionalAttribute} attribute1
|
||||
* @param {number} field2
|
||||
* @param {cqi.models.attributes.PositionalAttribute} attribute2
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async fdist2(cutoff, field1, attribute1, field2, attribute2) {
|
||||
return await this.client.api.cqp_fdist_2(
|
||||
this.apiName,
|
||||
cutoff,
|
||||
field1,
|
||||
attribute1.apiName,
|
||||
field2,
|
||||
attribute2.apiName
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
cqi.models.subcorpora.SubcorpusCollection = class SubcorpusCollection extends cqi.models.resource.Collection {
|
||||
/** @type {typeof cqi.models.subcorpora.Subcorpus} */
|
||||
static model = cqi.models.subcorpora.Subcorpus;
|
||||
|
||||
/**
|
||||
* @param {cqi.CQiClient} client
|
||||
* @param {cqi.models.corpora.Corpus} corpus
|
||||
*/
|
||||
constructor(client, corpus) {
|
||||
super(client);
|
||||
/** @type {cqi.models.corpora.Corpus} */
|
||||
this.corpus = corpus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(subcorpusName) {
|
||||
/** @type {string} */
|
||||
let apiName = `${this.corpus.apiName}:${subcorpusName}`;
|
||||
/** @type {object} */
|
||||
let fields = {};
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCH)) {
|
||||
fields.match = cqi.CONST_FIELD_MATCH;
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCHEND)) {
|
||||
fields.matchend = cqi.CONST_FIELD_MATCHEND
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_TARGET)) {
|
||||
fields.target = cqi.CONST_FIELD_TARGET
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_KEYWORD)) {
|
||||
fields.keyword = cqi.CONST_FIELD_KEYWORD
|
||||
}
|
||||
return {
|
||||
api_name: apiName,
|
||||
fields: fields,
|
||||
name: subcorpusName,
|
||||
size: await this.client.api.cqp_subcorpus_size(apiName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @returns {Promise<cqi.models.subcorpora.Subcorpus>}
|
||||
*/
|
||||
async get(subcorpusName) {
|
||||
return this.prepareModel(await this._get(subcorpusName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<cqi.models.subcorpora.Subcorpus[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let subcorpusNames = await this.client.api.cqp_list_subcorpora(this.corpus.apiName);
|
||||
/** @type {cqi.models.subcorpora.Subcorpus[]} */
|
||||
let subcorpora = [];
|
||||
for (let subcorpusName of subcorpusNames) {
|
||||
subcorpora.push(await this.get(subcorpusName));
|
||||
}
|
||||
return subcorpora;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user