mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-08 19:50:47 +00:00
Add support for removing words from user dictionaries to admin panel (#2371)
* Add support for deletion of words from user dictionary to admin-panel Co-authored-by: Jessica Lawshe <jessica.lawshe@overleaf.com> * Add confirmation modal to dictionary word deletion * Improve dictionary view with some helpful text * Add MockSpellingApi * Handle errors more cleanly in SpellingHandler GitOrigin-RevId: a7d7f8bad120a15b0eaa7d77b5ee804998477ed1
This commit is contained in:
parent
e4d180955f
commit
81103c93e6
3 changed files with 282 additions and 0 deletions
94
services/web/app/src/Features/Spelling/SpellingHandler.js
Normal file
94
services/web/app/src/Features/Spelling/SpellingHandler.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
const request = require('request')
|
||||
const Settings = require('settings-sharelatex')
|
||||
const OError = require('@overleaf/o-error')
|
||||
|
||||
const TIMEOUT = 10 * 1000
|
||||
|
||||
module.exports = {
|
||||
getUserDictionary(userId, callback) {
|
||||
const url = `${Settings.apis.spelling.url}/user/${userId}`
|
||||
request.get({ url: url, timeout: TIMEOUT }, (error, response) => {
|
||||
if (error) {
|
||||
return callback(
|
||||
new OError({
|
||||
message: 'error getting user dictionary',
|
||||
info: { error, userId }
|
||||
}).withCause(error)
|
||||
)
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return callback(
|
||||
new OError({
|
||||
message:
|
||||
'Non-success code from spelling API when getting user dictionary',
|
||||
info: { userId, statusCode: response.statusCode }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
callback(null, JSON.parse(response.body))
|
||||
})
|
||||
},
|
||||
|
||||
deleteWordFromUserDictionary(userId, word, callback) {
|
||||
const url = `${Settings.apis.spelling.url}/user/${userId}/unlearn`
|
||||
request.post(
|
||||
{
|
||||
url: url,
|
||||
json: {
|
||||
word
|
||||
},
|
||||
timeout: TIMEOUT
|
||||
},
|
||||
(error, response) => {
|
||||
if (error) {
|
||||
return callback(
|
||||
new OError({
|
||||
message: 'error deleting word from user dictionary',
|
||||
info: { error, userId, word }
|
||||
}).withCause(error)
|
||||
)
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return callback(
|
||||
new OError({
|
||||
message:
|
||||
'Non-success code from spelling API when removing word from user dictionary',
|
||||
info: { userId, word, statusCode: response.statusCode }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
deleteUserDictionary(userId, callback) {
|
||||
const url = `${Settings.apis.spelling.url}/user/${userId}`
|
||||
request.delete({ url: url, timeout: TIMEOUT }, (error, response) => {
|
||||
if (error) {
|
||||
return callback(
|
||||
new OError({
|
||||
message: 'error deleting user dictionary',
|
||||
info: { userId }
|
||||
}).withCause(error)
|
||||
)
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return callback(
|
||||
new OError({
|
||||
message:
|
||||
'Non-success code from spelling API when removing user dictionary',
|
||||
info: { userId, statusCode: response.statusCode }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
}
|
59
services/web/test/acceptance/src/helpers/MockSpellingApi.js
Normal file
59
services/web/test/acceptance/src/helpers/MockSpellingApi.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
const MockSpellingApi = {
|
||||
words: {},
|
||||
|
||||
run() {
|
||||
app.get('/user/:userId', (req, res) => {
|
||||
const { userId } = req.params
|
||||
const words = this.words[userId] || []
|
||||
res.json(words)
|
||||
})
|
||||
|
||||
app.delete('/user/:userId', (req, res) => {
|
||||
const { userId } = req.params
|
||||
this.words.delete(userId)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post('/user/:userId/learn', (req, res) => {
|
||||
const word = req.body.word
|
||||
const { userId } = req.params
|
||||
if (word) {
|
||||
this.words[userId] = this.words[userId] || []
|
||||
if (!this.words[userId].includes(word)) {
|
||||
this.words[userId].push(word)
|
||||
}
|
||||
}
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post('/user/:userId/unlearn', (req, res) => {
|
||||
const word = req.body.word
|
||||
const { userId } = req.params
|
||||
if (word && this.words[userId]) {
|
||||
const wordIndex = this.words[userId].indexOf(word)
|
||||
if (wordIndex !== -1) {
|
||||
this.words[userId].splice(wordIndex, 1)
|
||||
}
|
||||
}
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app
|
||||
.listen(3005, error => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
.on('error', error => {
|
||||
console.error('error starting MockSpellingApi:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
MockSpellingApi.run()
|
||||
|
||||
module.exports = MockSpellingApi
|
129
services/web/test/unit/src/Spelling/SpellingHandlerTests.js
Normal file
129
services/web/test/unit/src/Spelling/SpellingHandlerTests.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('path').join(
|
||||
__dirname,
|
||||
'../../../../app/src/Features/Spelling/SpellingHandler.js'
|
||||
)
|
||||
|
||||
const TIMEOUT = 1000 * 10
|
||||
|
||||
const SPELLING_HOST = 'http://spelling.service.test'
|
||||
const SPELLING_URL = 'http://spelling.service.test'
|
||||
|
||||
describe('SpellingHandler', function() {
|
||||
let userId, word, dictionary, dictionaryString, request, SpellingHandler
|
||||
|
||||
beforeEach(function() {
|
||||
userId = 'wombat'
|
||||
word = 'potato'
|
||||
dictionary = ['wombaat', 'woombat']
|
||||
dictionaryString = JSON.stringify(dictionary)
|
||||
request = {
|
||||
get: sinon
|
||||
.stub()
|
||||
.yields(null, { statusCode: 200, body: dictionaryString }),
|
||||
post: sinon.stub().yields(null, { statusCode: 204 }),
|
||||
delete: sinon.stub().yields(null, { statusCode: 204 })
|
||||
}
|
||||
|
||||
SpellingHandler = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
request: request,
|
||||
'logger-sharelatex': {
|
||||
warn() {},
|
||||
error() {},
|
||||
info() {}
|
||||
},
|
||||
'settings-sharelatex': {
|
||||
apis: { spelling: { host: SPELLING_HOST, url: SPELLING_URL } }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUserDictionary', function() {
|
||||
it('calls the spelling API', function(done) {
|
||||
SpellingHandler.getUserDictionary(userId, () => {
|
||||
expect(request.get).to.have.been.calledWith({
|
||||
url: 'http://spelling.service.test/user/wombat',
|
||||
timeout: TIMEOUT
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the dictionary', function(done) {
|
||||
SpellingHandler.getUserDictionary(userId, (err, dictionary) => {
|
||||
expect(err).not.to.exist
|
||||
expect(dictionary).to.deep.equal(dictionary)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an error when the request fails', function(done) {
|
||||
request.get = sinon.stub().yields(new Error('ugh'))
|
||||
SpellingHandler.getUserDictionary(userId, err => {
|
||||
expect(err).to.exist
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteWordFromUserDictionary', function() {
|
||||
it('calls the spelling API', function(done) {
|
||||
SpellingHandler.deleteWordFromUserDictionary(userId, word, () => {
|
||||
expect(request.post).to.have.been.calledWith({
|
||||
url: 'http://spelling.service.test/user/wombat/unlearn',
|
||||
json: {
|
||||
word: word
|
||||
},
|
||||
timeout: TIMEOUT
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not return an error', function(done) {
|
||||
SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
|
||||
expect(err).not.to.exist
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an error when the request fails', function(done) {
|
||||
request.post = sinon.stub().yields(new Error('ugh'))
|
||||
SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
|
||||
expect(err).to.exist
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteUserDictionary', function() {
|
||||
it('calls the spelling API', function(done) {
|
||||
SpellingHandler.deleteUserDictionary(userId, () => {
|
||||
expect(request.delete).to.have.been.calledWith({
|
||||
url: 'http://spelling.service.test/user/wombat',
|
||||
timeout: TIMEOUT
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not return an error', function(done) {
|
||||
SpellingHandler.deleteUserDictionary(userId, err => {
|
||||
expect(err).not.to.exist
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an error when the request fails', function(done) {
|
||||
request.delete = sinon.stub().yields(new Error('ugh'))
|
||||
SpellingHandler.deleteUserDictionary(userId, err => {
|
||||
expect(err).to.exist
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue