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

N21-2151 Partial course sync #5314

Merged
merged 31 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38e1c1b
make all classes available to teachers for course sync, when they hav…
MarvinOehlerkingCap Oct 24, 2024
8184c6e
documentation for studentVisibility feature
MarvinOehlerkingCap Oct 25, 2024
a0eeb0d
merge
MarvinOehlerkingCap Oct 24, 2024
b2a95b1
documentation for studentVisibility feature
MarvinOehlerkingCap Oct 25, 2024
0b3226a
Merge remote-tracking branch 'origin/N21-2151-partial-course-sync' in…
MarvinOehlerkingCap Oct 25, 2024
bd2bc9a
fix lint
MarvinOehlerkingCap Oct 25, 2024
e9a0ef8
add course sync attributes; update course-sync controller + uc + serv…
sdinkov Oct 28, 2024
52388b4
fix imports
sdinkov Oct 28, 2024
4b348a9
fix linter error
sdinkov Oct 28, 2024
2fde335
Merge remote-tracking branch 'origin/N21-2151-course-partial-sync-ext…
MarvinOehlerkingCap Oct 29, 2024
46065da
fix test
MarvinOehlerkingCap Oct 29, 2024
934f66a
fix test
MarvinOehlerkingCap Oct 29, 2024
4b55ef7
fix test coverage for things that I did not write
MarvinOehlerkingCap Oct 29, 2024
cc5c2b7
add seed data
MBergCap Oct 29, 2024
193c9c6
Merge branch 'N21-2151-partial-course-sync' of https://github.com/hpi…
MBergCap Oct 29, 2024
f1e3e6c
update partial sync
sdinkov Oct 30, 2024
7d8fdad
update course sync
sdinkov Nov 4, 2024
deee95d
fix repo test
sdinkov Nov 4, 2024
c57013b
Merge branch 'main' into N21-2151-partial-course-sync
sdinkov Nov 4, 2024
2f45f75
fix up coverage addUserToGroup
sdinkov Nov 4, 2024
ef64114
fix import
sdinkov Nov 4, 2024
8844a59
update course api test
sdinkov Nov 4, 2024
c084a52
fix tzpo
sdinkov Nov 4, 2024
66de6e8
update sync
sdinkov Nov 5, 2024
bd1513b
Merge branch 'main' into N21-2151-partial-course-sync
sdinkov Nov 5, 2024
5a2f948
update sync logic
sdinkov Nov 6, 2024
783776b
Merge branch 'main' into N21-2151-partial-course-sync
sdinkov Nov 6, 2024
b9bf1e3
update sync logic
sdinkov Nov 7, 2024
eb47e2d
Merge branch 'main' into N21-2151-partial-course-sync
sdinkov Nov 7, 2024
3e45e9c
extend course schema
sdinkov Nov 7, 2024
a50c07b
Merge branch 'main' into N21-2151-partial-course-sync
sdinkov Nov 7, 2024
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
191 changes: 191 additions & 0 deletions apps/server/src/modules/group/domain/group-aggregate.scope.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { GroupEntityTypes } from '../entity';
import { GroupAggregateScope } from './group-aggregate.scope';
import { GroupTypes } from './group-types';
import { GroupVisibilityPermission } from './group-visibility-permission.enum';

describe(GroupAggregateScope.name, () => {
const defaultFacetQuery = {
$facet: {
data: [{ $skip: 0 }],
total: [{ $count: 'count' }],
},
};

describe('byUserPermission', () => {
describe('when the user is allowed to see all groups of a school', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const schoolId = new ObjectId().toString();

return {
userId,
schoolId,
};
};

it('should build the correct query', () => {
const { userId, schoolId } = setup();

const result = new GroupAggregateScope()
.byUserPermission(userId, schoolId, GroupVisibilityPermission.ALL_SCHOOL_GROUPS)
.build();

expect(result).toEqual([{ $match: { organization: new ObjectId(schoolId) } }, defaultFacetQuery]);
});
});

describe('when the user is allowed to see his own groups and all classes of a school', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const schoolId = new ObjectId().toHexString();

return {
userId,
schoolId,
};
};

it('should build the correct query', () => {
const { userId, schoolId } = setup();

const result = new GroupAggregateScope()
.byUserPermission(userId, schoolId, GroupVisibilityPermission.ALL_SCHOOL_CLASSES)
.build();

expect(result).toEqual([
{
$match: {
$or: [
{ organization: new ObjectId(schoolId), type: GroupEntityTypes.CLASS },
{ users: { $elemMatch: { user: new ObjectId(userId) } } },
],
},
},
defaultFacetQuery,
]);
});
});

describe('when the user is allowed to only see his own groups', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const schoolId = new ObjectId().toHexString();

return {
userId,
schoolId,
};
};

it('should build the correct query', () => {
const { userId, schoolId } = setup();

const result = new GroupAggregateScope()
.byUserPermission(userId, schoolId, GroupVisibilityPermission.OWN_GROUPS)
.build();

expect(result).toEqual([
{ $match: { users: { $elemMatch: { user: new ObjectId(userId) } } } },
defaultFacetQuery,
]);
});
});
});

describe('byAvailableForSync', () => {
describe('when filtering for groups that are available for a course synchronization', () => {
it('should build the correct query', () => {
const result = new GroupAggregateScope().byAvailableForSync(true).build();

expect(result).toEqual([
{
$lookup: {
from: 'courses',
localField: '_id',
foreignField: 'syncedWithGroup',
as: 'syncedCourses',
},
},
{
$match: {
$or: [{ syncedCourses: { $size: 0 } }, { type: { $eq: GroupTypes.CLASS } }],
},
},
defaultFacetQuery,
]);
});
});

describe('when no value was given', () => {
it('should not include the query in the result', () => {
const result = new GroupAggregateScope().byAvailableForSync(undefined).build();

expect(result).toEqual([defaultFacetQuery]);
});
});
});

describe('byOrganization', () => {
describe('when filtering for an organization of a group', () => {
it('should build the correct query', () => {
const schoolId = new ObjectId().toHexString();

const result = new GroupAggregateScope().byOrganization(schoolId).build();

expect(result).toEqual([{ $match: { organization: new ObjectId(schoolId) } }, defaultFacetQuery]);
});
});

describe('when no value was given', () => {
it('should not include the query in the result', () => {
const result = new GroupAggregateScope().byOrganization(undefined).build();

expect(result).toEqual([defaultFacetQuery]);
});
});
});

describe('byUser', () => {
describe('when filtering for a group user', () => {
it('should build the correct query', () => {
const userId = new ObjectId().toHexString();

const result = new GroupAggregateScope().byUser(userId).build();

expect(result).toEqual([
{ $match: { users: { $elemMatch: { user: new ObjectId(userId) } } } },
defaultFacetQuery,
]);
});
});

describe('when no value was given', () => {
it('should not include the query in the result', () => {
const result = new GroupAggregateScope().byUser(undefined).build();

expect(result).toEqual([defaultFacetQuery]);
});
});
});

describe('byName', () => {
describe('when filtering for a group name', () => {
it('should build the correct query', () => {
const testName = 'testGroup';

const result = new GroupAggregateScope().byName(testName).build();

expect(result).toEqual([{ $match: { name: { $regex: testName, $options: 'i' } } }, defaultFacetQuery]);
});
});

describe('when no value was given', () => {
it('should not include the query in the result', () => {
const result = new GroupAggregateScope().byName(undefined).build();

expect(result).toEqual([defaultFacetQuery]);
});
});
});
});
78 changes: 78 additions & 0 deletions apps/server/src/modules/group/domain/group-aggregate.scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { StringValidator } from '@shared/common';
import { EntityId } from '@shared/domain/types';
import { MongoDbScope, MongoPatterns } from '@shared/repo';
import { GroupEntity, GroupEntityTypes } from '../entity';
import { GroupTypes } from './group-types';
import { GroupVisibilityPermission } from './group-visibility-permission.enum';

export class GroupAggregateScope extends MongoDbScope<GroupEntity> {
byUserPermission(userId: EntityId, schoolId: EntityId, permission: GroupVisibilityPermission): this {
if (permission === GroupVisibilityPermission.ALL_SCHOOL_GROUPS) {
this.byOrganization(schoolId);
} else if (permission === GroupVisibilityPermission.ALL_SCHOOL_CLASSES) {
this.pipeline.push({
$match: {
$or: [
{ organization: new ObjectId(schoolId), type: GroupEntityTypes.CLASS },
{ users: { $elemMatch: { user: new ObjectId(userId) } } },
],
},
});
} else {
this.byUser(userId);
}

return this;
}

byAvailableForSync(value: boolean | undefined): this {
if (value) {
this.pipeline.push(
{
$lookup: {
from: 'courses',
localField: '_id',
foreignField: 'syncedWithGroup',
as: 'syncedCourses',
},
},
{
$match: {
$or: [{ syncedCourses: { $size: 0 } }, { type: { $eq: GroupTypes.CLASS } }],
},
}
);
}

return this;
}

byUser(id: EntityId | undefined): this {
if (id) {
this.pipeline.push({ $match: { users: { $elemMatch: { user: new ObjectId(id) } } } });
}

return this;
}

byOrganization(id: EntityId | undefined): this {
if (id) {
this.pipeline.push({ $match: { organization: new ObjectId(id) } });
}

return this;
}

byName(nameQuery: string | undefined): this {
const escapedName: string | undefined = nameQuery
?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '')
.trim();

if (StringValidator.isNotEmptyString(escapedName, true)) {
this.pipeline.push({ $match: { name: { $regex: escapedName, $options: 'i' } } });
}

return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum GroupVisibilityPermission {
OWN_GROUPS,
ALL_SCHOOL_CLASSES,
ALL_SCHOOL_GROUPS,
}
2 changes: 2 additions & 0 deletions apps/server/src/modules/group/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export { GroupPeriod } from './group-period';
export * from './group-types';
export { GroupDeletedEvent } from './event';
export { GroupFilter } from './interface';
export { GroupAggregateScope } from './group-aggregate.scope';
export { GroupVisibilityPermission } from './group-visibility-permission.enum';
Loading
Loading