Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions codewit/api/src/controllers/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CourseModules,
Language,
Module,
ModuleResources,
Demo,
Resource,
UserDemoCompletion,
Expand Down Expand Up @@ -223,7 +224,10 @@ async function getAllCourses(): Promise<CourseResponse[]> {
{ association: Course.associations.instructors },
{ association: Course.associations.roster },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
});
return formatCourseResponse(courses);
}
Expand All @@ -241,7 +245,10 @@ async function getTeacherCourses(teacherId: string): Promise<CourseResponse[]> {
{ association: Course.associations.instructors, where: { googleId: teacherId } },
{ association: Course.associations.roster },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
});

return formatCourseResponse(courses);
Expand All @@ -259,7 +266,10 @@ async function getStudentCourses(studentId: string): Promise<CourseResponse[]> {
{ association: Course.associations.instructors },
{ association: Course.associations.roster, where: { googleId: studentId } },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
});

return formatCourseResponse(courses, true);
Expand All @@ -277,7 +287,10 @@ async function getStudentCoursesByUid(userUid: number): Promise<CourseResponse[]
{ association: Course.associations.instructors },
{ association: Course.associations.roster, where: { uid: userUid } },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
});

return formatCourseResponse(courses, true);
Expand All @@ -303,7 +316,10 @@ export async function getStudentCourse(course_id: string, transaction?: Transact
},
{ association: Course.associations.instructors },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
transaction,
});

Expand Down
31 changes: 27 additions & 4 deletions codewit/api/src/controllers/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Demo, Language, Module, Resource, sequelize } from '../models';
import { Demo, Language, Module, ModuleResources, Resource, sequelize } from '../models';
import { ModuleResponse } from '../typings/response.types';
import { formatModuleResponse } from '../utils/responseFormatter';

Expand Down Expand Up @@ -30,8 +30,19 @@ async function createModule(
await module.setDemos(demos, { transaction });
await module.setLanguage(languageInstance, { transaction });

await module.setResources(resources, { transaction });
await module.reload({ include: [Language, Demo, Resource], transaction });
await ModuleResources.bulkCreate(
resources.map((resourceUid, ordering) => ({
moduleUid: module.uid,
resourceUid,
ordering,
})),
{ transaction },
);
await module.reload({
include: [Language, Demo, Resource],
order: [[Module.associations.resources, ModuleResources, 'ordering', 'ASC']],
transaction,
});

return formatModuleResponse(module);
});
Expand All @@ -40,6 +51,7 @@ async function createModule(
async function getModule(uid: number): Promise<ModuleResponse | null> {
const module = await Module.findByPk(uid, {
include: [Language, Demo, Resource],
order: [[Module.associations.resources, ModuleResources, 'ordering', 'ASC']],
});

return formatModuleResponse(module);
Expand Down Expand Up @@ -72,7 +84,15 @@ async function updateModule(
await module.setLanguage(languageInstance, { transaction });
}
if (resources) {
await module.setResources(resources, { transaction });
await ModuleResources.destroy({ where: { moduleUid: module.uid }, transaction });
await ModuleResources.bulkCreate(
resources.map((resourceUid, ordering) => ({
moduleUid: module.uid,
resourceUid,
ordering,
})),
{ transaction },
);
}

await module.save({ transaction });
Expand All @@ -94,6 +114,7 @@ async function updateModule(

await module.reload({
include: [Language, Demo, Resource],
order: [[Module.associations.resources, ModuleResources, 'ordering', 'ASC']],
transaction,
});

Expand All @@ -104,6 +125,7 @@ async function updateModule(
async function getModules(): Promise<ModuleResponse[]> {
const modules = await Module.findAll({
include: [Language, Demo, Resource],
order: [[Module.associations.resources, ModuleResources, 'ordering', 'ASC']],
});

return formatModuleResponse(modules);
Expand All @@ -112,6 +134,7 @@ async function getModules(): Promise<ModuleResponse[]> {
async function deleteModule(uid: number): Promise<ModuleResponse | null> {
const module = await Module.findByPk(uid, {
include: [Language, Demo, Resource],
order: [[Module.associations.resources, ModuleResources, 'ordering', 'ASC']],
});
if (!module) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async transaction => {
await queryInterface.addColumn(
'ModuleResources',
'ordering',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
{ transaction },
);

await queryInterface.sequelize.query(
`
with ordered_resources as (
select "moduleUid" as module_uid,
"resourceUid" as resource_uid,
row_number() over (
partition by "moduleUid"
order by "createdAt", "resourceUid"
) - 1 as ordering
from "ModuleResources"
)
update "ModuleResources"
set ordering = ordered_resources.ordering
from ordered_resources
where "ModuleResources"."moduleUid" = ordered_resources.module_uid
and "ModuleResources"."resourceUid" = ordered_resources.resource_uid`,
{ type: Sequelize.QueryTypes.RAW, transaction },
);
}),
down: (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async transaction => {
await queryInterface.removeColumn('ModuleResources', 'ordering', { transaction });
}),
};
7 changes: 5 additions & 2 deletions codewit/api/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UserExerciseCompletion } from './userExerciseCompletion';
import { UserModuleCompletion } from './userModuleCompletion';
import { DemoExercises } from './demoExercises';
import { ModuleDemos } from './moduleDemos';
import { ModuleResources } from './moduleResources';

require('dotenv').config();

Expand Down Expand Up @@ -60,6 +61,7 @@ const sequelize = new Sequelize({
UserModuleCompletion,
DemoExercises,
ModuleDemos,
ModuleResources,
].forEach((model) => model.initialize(sequelize));

Demo.belongsToMany(Exercise, { through: DemoExercises });
Expand Down Expand Up @@ -96,8 +98,8 @@ Language.hasMany(Exercise, { foreignKey: 'languageUid' });
Demo.belongsToMany(Module, { through: ModuleDemos });
Module.belongsToMany(Demo, { through: ModuleDemos });

Resource.belongsToMany(Module, { through: 'ModuleResources' });
Module.belongsToMany(Resource, { through: 'ModuleResources' });
Resource.belongsToMany(Module, { through: ModuleResources });
Module.belongsToMany(Resource, { through: ModuleResources });

Module.belongsTo(Language);
Language.hasMany(Module);
Expand Down Expand Up @@ -175,6 +177,7 @@ export {
Course,
Module,
ModuleDemos,
ModuleResources,
CourseModules,
CourseRegistration,
Resource,
Expand Down
46 changes: 46 additions & 0 deletions codewit/api/src/models/moduleResources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Sequelize,
} from 'sequelize';

class ModuleResources extends Model<
InferAttributes<ModuleResources>,
InferCreationAttributes<ModuleResources>
> {
declare moduleUid: number;
declare resourceUid: number;
declare ordering: number;

static initialize(sequelize: Sequelize) {
this.init(
{
moduleUid: {
type: DataTypes.INTEGER,
primaryKey: true,
references: { model: 'modules', key: 'uid' },
},
resourceUid: {
type: DataTypes.INTEGER,
primaryKey: true,
references: { model: 'resources', key: 'uid' },
},
ordering: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
},
{
sequelize,
modelName: 'ModuleResources',
tableName: 'ModuleResources',
timestamps: true,
},
);
}
}

export { ModuleResources };
6 changes: 5 additions & 1 deletion codewit/api/src/routes/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CourseRegistration,
Resource,
Module,
ModuleResources,
} from '../models';
import { asyncHandle } from "../middleware/catch";
import { } from "../models";
Expand Down Expand Up @@ -275,7 +276,10 @@ courseRouter.get('/:uid', asyncHandle(async (req, res) => {
{ association: Course.associations.instructors, where: { uid: req.user.uid } },
{ association: Course.associations.roster },
],
order: [[Course.associations.modules, CourseModules, 'ordering', 'ASC']],
order: [
[Course.associations.modules, CourseModules, 'ordering', 'ASC'],
[Course.associations.modules, Module.associations.resources, ModuleResources, 'ordering', 'ASC'],
],
});

let result = formatCourseResponse(course, true);
Expand Down
10 changes: 7 additions & 3 deletions codewit/api/src/routes/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DemoAttempt } from "@codewit/interfaces";
import { fromZodError } from 'zod-validation-error';
import { checkAdmin } from '../middleware/auth';
import { asyncHandle } from '../middleware/catch';
import { Attempt, Demo, DemoTags, Language, sequelize, Tag, UserExerciseCompletion } from '../models';
import { Attempt, Demo, DemoExercises, DemoTags, Language, sequelize, Tag, UserExerciseCompletion } from '../models';
import { Op, QueryTypes } from 'sequelize';

const demoRouter = Router();
Expand Down Expand Up @@ -84,7 +84,10 @@ demoRouter.get("/:uid/attempt", asyncHandle(async (req, res) => {
Tag,
Language
],
order: [[Tag, DemoTags, "ordering", "ASC"]]
order: [
[Tag, DemoTags, "ordering", "ASC"],
[Demo.associations.exercises, DemoExercises, "order", "ASC"],
]
});

if (demo_record == null) {
Expand Down Expand Up @@ -166,7 +169,8 @@ demoRouter.get("/:uid/attempt", asyncHandle(async (req, res) => {
mod_resc."moduleUid" = $1
left join "ResourceLikes" as resc_likes on
resources.uid = resc_likes."resourceUid" and
resc_likes."userUid" = $2`,
resc_likes."userUid" = $2
order by mod_resc.ordering asc`,
{
type: QueryTypes.SELECT,
bind: [maybe_module_id, req.user.uid]
Expand Down
32 changes: 32 additions & 0 deletions codewit/client-e2e/src/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,9 +613,17 @@ describe("Module creations functionality", () => {
statusCode: 200,
body: []
}).as('getModules');
cy.intercept('GET', '/resources', {
statusCode: 200,
body: [
{ uid: 1, title: 'First resource', url: 'https://example.com/first', source: 'Example', likes: 0 },
{ uid: 2, title: 'Second resource', url: 'https://example.com/second', source: 'Example', likes: 0 },
],
}).as('getResources');
cy.visit('/create/module');
cy.wait('@getUserInfo');
cy.wait('@getModules');
cy.wait('@getResources');
})

it('should render successfully', () => {
Expand All @@ -642,6 +650,30 @@ describe("Module creations functionality", () => {
getSubmitButton().click();
cy.wait('@createModule');
})

it('adds, reorders, and removes selected resources before saving', () => {
cy.intercept('POST', '/modules', (req) => {
expect(req.body).to.deep.equal({
language: 'cpp',
resources: [2],
topic: 'operation',
});
}).as('createOrderedModule');

cy.contains('Create Module').click();
getTopicSelect().type('operation{enter}');
getLanguageSelect().type('cpp{enter}');
cy.get('#resource-select').type('First resource{enter}');
cy.get('#resource-select').type('Second resource{enter}');

cy.get('[data-testid="selected-resources"]').should('contain.text', 'First resource');
cy.get('[data-testid="selected-resources"]').should('contain.text', 'Second resource');
cy.get('[aria-label="Drag Second resource"]').focus().type('{space}{uparrow}{space}');
cy.get('[aria-label="Remove First resource"]').click();

getSubmitButton().click();
cy.wait('@createOrderedModule');
});
})


Expand Down
Loading
Loading