Skip to content
This repository has been archived by the owner on May 10, 2023. It is now read-only.

Commit

Permalink
feat: add sentence overview and possibility to delete own sentences (f…
Browse files Browse the repository at this point in the history
…ixes #118)
  • Loading branch information
MichaelKohler committed Jun 27, 2021
1 parent c5154b1 commit 94bd170
Show file tree
Hide file tree
Showing 19 changed files with 766 additions and 4 deletions.
44 changes: 44 additions & 0 deletions server/lib/sentences.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = {
getRejectedSentencesForLocale,
getSentencesForReview,
getRejectedSentences,
getMySentences,
deleteMySentences,
getStats,
getUserAddedSentencesPerLocale,
getUnreviewedByYouCountForLocales,
Expand Down Expand Up @@ -88,6 +90,48 @@ async function getRejectedSentences({ userId }) {
return sentencesPerLocale;
}

async function getMySentences({ userId }) {
debug('GETTING_MY_SENTENCES');

const options = {
where: {
userId,
},
};

const sentences = await Sentence.findAll(options);

const sentencesPerLocale = sentences.reduce((perLocale, sentenceInfo) => {
perLocale[sentenceInfo.localeId] = perLocale[sentenceInfo.localeId] || {};

const batch = sentenceInfo.batch || 0;
perLocale[sentenceInfo.localeId][batch] = perLocale[sentenceInfo.localeId][batch] || {
source: sentenceInfo.source,
sentences: []
};
perLocale[sentenceInfo.localeId][batch].sentences.push(sentenceInfo);

return perLocale;
}, {});

return sentencesPerLocale;
}

async function deleteMySentences({ userId, sentenceIds }) {
debug('DELETING_MY_SENTENCES');

const options = {
where: {
id: sentenceIds,
// Passing the userId here as well makes sure that sentences that
// do not belong to this user would silently be ignored.
userId,
},
};

await Sentence.destroy(options);
}

async function getStats(locales) {
debug('GETTING_STATS');

Expand Down
25 changes: 25 additions & 0 deletions server/routes/sentences.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ router.get('/rejected', async (req, res) => {
});
});

router.get('/my', async (req, res) => {
const userId = req.user && req.user.email;
debug('GET_MY_SENTENCES', userId);
sentences.getMySentences({ userId })
.then((foundSentences) => res.json(foundSentences))
.catch((error) => {
debug('GET_MY_SENTENCES_ERROR', error);
res.status(STATUS_ERROR);
res.json({ message: error.message });
});
});

router.post('/delete', async (req, res) => {
const userId = req.user && req.user.email;
const sentenceIds = req.body.sentences;
debug('DELETE_MY_SENTENCES', userId);
sentences.deleteMySentences({ userId, sentenceIds })
.then(() => res.json({}))
.catch((error) => {
debug('DELETE_SENTENCES_ERROR', error);
res.status(STATUS_ERROR);
res.json({ message: error.message });
});
});

router.put('/', async (req, res) => {
const userId = req.user && req.user.email;
debug('CREATE_SENTENCES', req.body, userId);
Expand Down
71 changes: 71 additions & 0 deletions server/tests/lib/sentences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test.beforeEach((t) => {
t.context.sandbox.stub(Sentence, 'count').resolves(0);
t.context.sandbox.stub(Sentence, 'create').resolves(exampleSentenceRecord);
t.context.sandbox.stub(Sentence, 'findAll').resolves([exampleSentenceRecord]);
t.context.sandbox.stub(Sentence, 'destroy').resolves();
t.context.sandbox.stub(sequelize, 'query').resolves([exampleSentenceRecord]);
t.context.transactionMock = {
commit: t.context.sandbox.stub(),
Expand Down Expand Up @@ -249,3 +250,73 @@ test.serial('getUserAddedSentencesPerLocale: should fetch user stats correctly',
},
});
});

test.serial('getMySentences: should fetch correctly', async (t) => {
const userId = ['foo'];
Sentence.findAll.resolves([{
id: 1,
sentence: 'Hi',
source: 'bla',
localeId: 'en',
batch: 1,
}, {
id: 2,
sentence: 'Hi there',
source: 'bla',
localeId: 'en',
batch: 1,
}, {
id: 3,
sentence: 'Hallo',
source: 'meh',
localeId: 'de',
batch: 2,
}]);

const stats = await sentences.getMySentences({ userId });
t.deepEqual(stats, {
en: {
'1': {
source: 'bla',
sentences: [{
id: 1,
sentence: 'Hi',
source: 'bla',
localeId: 'en',
batch: 1,
}, {
id: 2,
sentence: 'Hi there',
source: 'bla',
localeId: 'en',
batch: 1,
}],
},
},
de: {
'2': {
source: 'meh',
sentences: [{
id: 3,
sentence: 'Hallo',
source: 'meh',
localeId: 'de',
batch: 2,
}],
},
},
});
});

test.serial('deleteMySentences: should delete correctly', async (t) => {
const userId = ['foo'];
Sentence.destroy.resolves();

await sentences.deleteMySentences({ userId, sentenceIds: [1, 2] });
t.true(Sentence.destroy.calledWith({
where: {
id: [1, 2],
userId,
},
}));
});
46 changes: 46 additions & 0 deletions server/tests/routes/sentences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ test.beforeEach((t) => {
t.context.sandbox.stub(sentences, 'getRejectedSentencesForLocale').resolves(sentencesMock);
t.context.sandbox.stub(sentences, 'getSentencesForReview').resolves(sentencesMock);
t.context.sandbox.stub(sentences, 'getRejectedSentences').resolves(sentencesMock);
t.context.sandbox.stub(sentences, 'getMySentences').resolves(sentencesMock);
t.context.sandbox.stub(sentences, 'deleteMySentences').resolves({});
t.context.sandbox.stub(sentences, 'addSentences').resolves(sentencesMock);
});

Expand Down Expand Up @@ -258,6 +260,50 @@ test.serial('getting rejected sentences should pass on error message', async (t)
});
});

test.serial('should get my sentences', async (t) => {
const response = await request(app)
.get('/sentence-collector/sentences/my');

t.is(response.status, 200);
t.deepEqual(response.body, sentencesMock);
t.true(sentences.getMySentences.calledWith({ userId: undefined }));
});

test.serial('getting my sentences should pass on error message', async (t) => {
sentences.getMySentences.rejects(new Error('nope'));

const response = await request(app)
.get('/sentence-collector/sentences/my');

t.is(response.status, 500);
t.deepEqual(response.body, {
message: 'nope',
});
});

test.serial('should delete my sentences', async (t) => {
const response = await request(app)
.post('/sentence-collector/sentences/delete')
.send({ sentences: [1] });

t.is(response.status, 200);
t.log(sentences.deleteMySentences.getCall(0).args);
t.true(sentences.deleteMySentences.calledWith({ userId: undefined, sentenceIds: [1] }));
});

test.serial('deleting sentences should pass on error message', async (t) => {
sentences.deleteMySentences.rejects(new Error('nope'));

const response = await request(app)
.post('/sentence-collector/sentences/delete')
.send({ sentences: [1] });

t.is(response.status, 500);
t.deepEqual(response.body, {
message: 'nope',
});
});

test.serial('should add sentences', async (t) => {
const sentenceParams = {
sentence: 'Hi',
Expand Down
19 changes: 19 additions & 0 deletions web/css/sentences-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.language-section {
padding-bottom: 4rem;
}

.submission-section {
padding-bottom: 2rem;
}

.submission-section li input {
vertical-align: middle;
}

.submission-section li p {
margin-top: 0;
margin-bottom: 0;
display: inline-block;
margin-left: 1rem;
vertical-align: middle;
}
4 changes: 2 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@
],
"coverageThreshold": {
"global": {
"branches": 89,
"functions": 81,
"branches": 87,
"functions": 80,
"lines": 93,
"statements": 91
}
Expand Down
49 changes: 49 additions & 0 deletions web/src/actions/sentences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,55 @@ describe('loadRejectedSentences', () => {
});
});

describe('loadMySentences', () => {
test('should load my sentences', async () => {
(backend.sendRequest as jest.Mock).mockImplementation(() => exampleSentences);
await sentences.loadMySentences()(dispatch, getState, null);
expect((backend.sendRequest as jest.Mock).mock.calls[0][0]).toEqual('sentences/my');
expect(dispatch.mock.calls[0][0]).toEqual({
type: sentences.ACTION_LOAD_MY_SENTENCES,
});
expect(dispatch.mock.calls[1][0]).toEqual({
type: sentences.ACTION_GOT_MY_SENTENCES,
sentences: exampleSentences,
});
});

test('should not throw on error', async () => {
const error = new Error('NOPE');
(backend.sendRequest as jest.Mock).mockImplementation(() => { throw error; });
expect(sentences.loadMySentences()(dispatch, getState, null)).resolves.not.toThrow(error);
expect(dispatch.mock.calls[1][0]).toEqual({
type: sentences.ACTION_MY_SENTENCES_FAILURE,
errorMessage: 'NOPE',
});
});
});

describe('deleteSentences', () => {
test('should delete sentences', async () => {
(backend.sendRequest as jest.Mock).mockImplementation(() => ({}));
await sentences.deleteSentences([1])(dispatch, getState, null);
expect((backend.sendRequest as jest.Mock).mock.calls[0][0]).toEqual('sentences/delete');
expect(dispatch.mock.calls[0][0]).toEqual({
type: sentences.ACTION_DELETE_SENTENCES,
});
expect(dispatch.mock.calls[1][0]).toEqual({
type: sentences.ACTION_DELETE_SENTENCES_DONE,
});
});

test('should not throw on error', async () => {
const error = new Error('NOPE');
(backend.sendRequest as jest.Mock).mockImplementation(() => { throw error; });
expect(sentences.deleteSentences([1])(dispatch, getState, null)).resolves.not.toThrow(error);
expect(dispatch.mock.calls[1][0]).toEqual({
type: sentences.ACTION_DELETE_SENTENCES_FAILURE,
errorMessage: 'NOPE',
});
});
});

describe('resetReviewMessage', () => {
test('should reset message', async () => {
await sentences.resetReviewMessage()(dispatch, getState, null);
Expand Down
Loading

0 comments on commit 94bd170

Please sign in to comment.