Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Routes for group specific vocab activation #94

Merged
merged 11 commits into from
Feb 26, 2023
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