Skip to content

Commit

Permalink
Refactor: Routes for group specific vocab activation (#94)
Browse files Browse the repository at this point in the history
* refactor: getGroups route to return staged

* refactor: get group vocabs staged flag

* refactor: conditional if in where query

* feat: updated query route for custom learning

* fix: groupId specific filtering

* feat: optional onlyActivated prop for routes

* fix: query routes for custom learning

* docs: updated swagger

* fix: random vocab return for custom learning

* docs: updated comment

* lint: if query

---------

Co-authored-by: noctera <noctera.dev@proton.me>
  • Loading branch information
noctera and noctera authored Feb 26, 2023
1 parent c04105d commit bcff51e
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 31 deletions.
16 changes: 11 additions & 5 deletions app/Controllers/GroupController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { createGroup, getGroups, destroyGroup, updateGroup } = require('../Services/GroupServiceProvider.js');
const { getStats } = require('../Services/StatsServiceProvider.js');
const ApiError = require('../utils/ApiError.js');
const httpStatus = require('http-status');
const catchAsync = require('../utils/catchAsync');

const addGroup = catchAsync(async (req, res) => {
Expand All @@ -24,14 +26,19 @@ const sendGroups = catchAsync(async (req, res) => {

// decide if we have to fetch stats
const includeStats = (req.query.stats || false) === 'true';
const onlyStaged = (req.query.onlyStaged || false) === 'true';
const onlyActivated = (req.query.onlyActivated || false) === 'true';

// get groups
const groups = await getGroups(userId, languagePackageId);
if (onlyStaged && onlyActivated) {
throw new ApiError(httpStatus.BAD_REQUEST, 'you can not select both onlyStaged and onlyActivated');
}

// get groups
const groups = await getGroups(userId, languagePackageId, onlyStaged, onlyActivated);
const formatted = await Promise.all(
groups.map(async (group) => ({
...group.toJSON(),

// if onlyStaged or onlyActivated return just group, as response has already been prepared
...(onlyStaged || onlyActivated ? group : { ...group.toJSON() }),
...(includeStats
? {
stats: await getStats({
Expand All @@ -43,7 +50,6 @@ const sendGroups = catchAsync(async (req, res) => {
: {}),
}))
);

res.send(formatted);
});

Expand Down
3 changes: 2 additions & 1 deletion app/Controllers/LanguagePackageController.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ const sendLanguagePackages = catchAsync(async (req, res) => {
const userId = req.user.id;
const includeGroups = (req.query.groups || false) === 'true';
const includeStats = (req.query.stats || false) === 'true';
const onlyActivated = (req.query.onlyActivated || false) === 'true';

// get language Package
const languagePackages = await getLanguagePackages(userId, includeGroups);
const languagePackages = await getLanguagePackages(userId, includeGroups, onlyActivated);

const formatted = await Promise.all(
languagePackages.map(async (languagePackage) => ({
Expand Down
46 changes: 39 additions & 7 deletions app/Controllers/QueryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,54 @@ const {
getNumberOfUnresolvedVocabulary,
getNumberOfLearnedTodayVocabulary,
} = require('../Services/StatsServiceProvider.js');
const { getGroupsVocabulary } = require('../Services/VocabularyServiceProvider.js');
const catchAsync = require('../utils/catchAsync');

const sendQueryVocabulary = catchAsync(async (req, res) => {
// get userId from request
const userId = req.user.id;
const { languagePackageId } = req.params;
const { limit } = { limit: '100', ...req.query };
const { staged } = { staged: false, ...req.query };
// convert to bool
const isStaged = staged === 'true';
const onlyStaged = (req.query.onlyStaged || false) === 'true';
const onlyActivated = (req.query.onlyActivated || false) === 'true';
let { groupId } = { groupId: null, ...req.query };

// if staged = true return the staged vocabulary
if (isStaged) {
const vocabulary = await getUnactivatedVocabulary(languagePackageId, userId);
// convert groups to Array, if only one group was sent. Express is storing it a string instead of an Array
if (!Array.isArray(groupId)) {
if (groupId !== null) {
groupId = [groupId];
}
}

// only staged vocabs
if (onlyStaged) {
// if group ids are set, only return staged vocabs from that groups
if (groupId) {
// specific vocab activation
const vocabulary = await getUnactivatedVocabulary(languagePackageId, userId, groupId);
res.send(vocabulary);
} else {
// return all unactivated vocabs
const vocabulary = await getUnactivatedVocabulary(languagePackageId, userId, groupId);
res.send(vocabulary);
}
}

// custom learning with only activated vocabs
if (!onlyStaged && onlyActivated && groupId) {
const vocabulary = await getGroupsVocabulary(userId, groupId, false, true, true);
res.send(vocabulary);
} else {
}

// custom learning with activated and staged vocabs
if (!onlyStaged && !onlyActivated && groupId) {
const vocabulary = await getGroupsVocabulary(userId, groupId, false, false, true);
res.send(vocabulary);
}

// regular daily query
if (!onlyStaged && onlyActivated && !groupId) {
// if no groups are set, just return vocabs depending on the learning algorithm
const vocabulary = await getQueryVocabulary(languagePackageId, userId, limit);
res.send(vocabulary);
}
Expand Down
3 changes: 2 additions & 1 deletion app/Controllers/VocabularyController.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ const sendGroupVocabulary = catchAsync(async (req, res) => {
const userId = req.user.id;
const { groupId } = req.params;
const { search } = req.query;
const onlyStaged = (req.query.onlyStaged || false) === 'true';

const vocabulary = await getGroupVocabulary(userId, groupId, search);
const vocabulary = await getGroupVocabulary(userId, groupId, search, onlyStaged);

res.send(vocabulary);
});
Expand Down
28 changes: 25 additions & 3 deletions app/Services/GroupServiceProvider.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { LanguagePackage, Group } = require('../../database');
const { LanguagePackage, Group, VocabularyCard, Drawer } = require('../../database');
const { deleteKeysFromObject } = require('../utils');
const ApiError = require('../utils/ApiError.js');
const httpStatus = require('http-status');
Expand All @@ -17,7 +17,7 @@ async function createGroup({ name, description, active }, userId, languagePackag
}

// get groups
async function getGroups(userId, languagePackageId) {
async function getGroups(userId, languagePackageId, onlyStaged, onlyActivated) {
const languagePackage = await LanguagePackage.count({
where: {
id: languagePackageId,
Expand All @@ -29,15 +29,37 @@ async function getGroups(userId, languagePackageId) {
throw new ApiError(httpStatus.NOT_FOUND, 'no groups found, because the language package does not exist');
}

// if only groups with staged vocabs should be returned, include vocabs with drawer stages to validate
const groups = await Group.findAll({
attributes: ['id', 'languagePackageId', 'name', 'description', 'active'],
include:
onlyStaged || onlyActivated
? [
{
model: VocabularyCard,
attributes: ['id'],
include: [
{
model: Drawer,
attributes: ['stage'],
},
],
},
]
: null,

where: {
userId,
languagePackageId,
...(onlyStaged && { '$VocabularyCards.active$': true } ? { '$VocabularyCards.Drawer.stage$': 0 } : null),
...(onlyActivated && { '$VocabularyCards.active$': true } ? { '$VocabularyCards.Drawer.stage$': !0 } : null),
},
});

return groups;
// if onlyStaged or onlyActivated, remove VocabularyCards from response
return onlyStaged || onlyActivated
? groups.map((group) => deleteKeysFromObject(['VocabularyCards'], group.dataValues))
: groups;
}

async function destroyGroup(userId, groupId) {
Expand Down
35 changes: 32 additions & 3 deletions app/Services/LanguagePackageServiceProvider.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { ForeignKeyConstraintError } = require('sequelize');
const { LanguagePackage, Group } = require('../../database');
const { LanguagePackage, Group, VocabularyCard, Drawer } = require('../../database');
const { deleteKeysFromObject } = require('../utils');
const ApiError = require('../utils/ApiError.js');
const httpStatus = require('http-status');
Expand Down Expand Up @@ -34,14 +34,43 @@ async function createLanguagePackage(
}

// get language package
async function getLanguagePackages(userId, groups) {
async function getLanguagePackages(userId, groups, onlyActivated) {
// Get user with email from database
const languagePackages = await LanguagePackage.findAll({
// if groups is true, return groups to every language package
include: groups ? [{ model: Group, attributes: ['id', 'name', 'description', 'active'] }] : [],
/* eslint-disable no-nested-ternary */
include: groups
? [
{
model: Group,
attributes: ['id', 'name', 'description', 'active'],
},
]
: onlyActivated
? [
{
model: Group,
attributes: ['id', 'name', 'description', 'active'],
include: [
{
model: VocabularyCard,
attributes: ['id'],
include: [
{
model: Drawer,
attributes: ['stage'],
},
],
},
],
},
]
: [],
/* eslint-enable no-nested-ternary */
attributes: ['id', 'name', 'foreignWordLanguage', 'translatedWordLanguage', 'vocabsPerDay', 'rightWords'],
where: {
userId,
...(onlyActivated && { '$Groups.active$': true } ? { '$Groups.VocabularyCards.Drawer.stage$': !0 } : null),
},
});

Expand Down
9 changes: 8 additions & 1 deletion app/Services/QueryServiceProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async function getQueryVocabulary(languagePackageId, userId, limit) {
}

// return the unactivated vocabulary
async function getUnactivatedVocabulary(languagePackageId, userId) {
async function getUnactivatedVocabulary(languagePackageId, userId, groupIds) {
// Get drawers id
const drawer = await Drawer.findOne({
attributes: ['id'],
Expand Down Expand Up @@ -120,6 +120,13 @@ async function getUnactivatedVocabulary(languagePackageId, userId) {
drawerId: drawer.id,
'$Group.active$': true,
active: true,
...(groupIds
? {
groupId: {
[Op.or]: [groupIds],
},
}
: null),
},
});

Expand Down
63 changes: 57 additions & 6 deletions app/Services/VocabularyServiceProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ const { VocabularyCard, Translation, Drawer, Group } = require('../../database')
const { deleteKeysFromObject } = require('../utils');
const ApiError = require('../utils/ApiError.js');
const httpStatus = require('http-status');
const sequelize = require('sequelize');
const { Op } = sequelize;
const { Sequelize, Op } = require('sequelize');

// create language package
async function createVocabularyCard({
Expand Down Expand Up @@ -74,7 +73,7 @@ async function createTranslations(translations, userId, languagePackageId, vocab
return false;
}

async function getGroupVocabulary(userId, groupId, search) {
async function getGroupVocabulary(userId, groupId, search, onlyStaged) {
const group = await Group.count({
where: {
id: groupId,
Expand All @@ -92,17 +91,25 @@ async function getGroupVocabulary(userId, groupId, search) {
model: Translation,
attributes: ['name'],
},
{
model: Drawer,
attributes: ['stage'],
},
],
attributes: ['id', 'name', 'active', 'description'],
where: {
[Op.and]: [
{ userId, groupId },
{
userId,
groupId,
...(onlyStaged ? { '$Drawer.stage$': 0 } : null),
},
search && {
[Op.or]: [
sequelize.where(sequelize.fn('lower', sequelize.col('VocabularyCard.name')), {
Sequelize.where(Sequelize.fn('lower', Sequelize.col('VocabularyCard.name')), {
[Op.like]: `%${search.toLowerCase()}%`,
}),
sequelize.where(sequelize.fn('lower', sequelize.col('Translations.name')), {
Sequelize.where(Sequelize.fn('lower', Sequelize.col('Translations.name')), {
[Op.like]: `%${search.toLowerCase()}%`,
}),
],
Expand All @@ -114,6 +121,49 @@ async function getGroupVocabulary(userId, groupId, search) {
return vocabulary;
}

// this function is the same as getGroupVocabulary, but for multiple group ids and without search functionality
// Because we don't use TypeScript watch out which one you use
// TODO: Maybe I will add those two functions together one time
async function getGroupsVocabulary(userId, groupIds, onlyStaged, onlyActivated, random) {
groupIds.map(async (groupId) => {
const group = await Group.count({
where: {
id: groupId,
userId,
},
});

if (group === 0) {
throw new ApiError(httpStatus.NOT_FOUND, 'no vocabulary cards found, because the group does not exist');
}
});

const vocabulary = await VocabularyCard.findAll({
include: [
{
model: Translation,
attributes: ['name'],
},
{
model: Drawer,
attributes: ['stage'],
},
],
order: random ? Sequelize.literal('random()') : null,
attributes: ['id', 'name', 'active', 'description'],
where: {
userId,
...(onlyStaged ? { '$Drawer.stage$': 0 } : null),
...(onlyActivated ? { '$Drawer.stage$': !0 } : null),
groupId: {
[Op.or]: [groupIds],
},
},
});

return vocabulary;
}

async function destroyVocabularyCard(userId, vocabularyCardId) {
const counter = await VocabularyCard.destroy({
where: {
Expand Down Expand Up @@ -180,5 +230,6 @@ module.exports = {
createTranslations,
destroyVocabularyCard,
getGroupVocabulary,
getGroupsVocabulary,
updateVocabulary,
};
Loading

0 comments on commit bcff51e

Please sign in to comment.