Skip to content
Merged
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
30 changes: 19 additions & 11 deletions backend/extra/seedDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,8 @@ import { AssignmentRepositoryTypeORM } from "../src/infrastructure/repositories/
import { GroupRepositoryTypeORM } from "../src/infrastructure/repositories/groupRepositoryTypeORM";
import { ThreadRepositoryTypeORM } from "../src/infrastructure/repositories/questionThreadRepositoryTypeORM";
import { MessageRepositoryTypeORM } from "../src/infrastructure/repositories/messageRepositoryTypeORM";
import { JoinRequestType } from "../src/core/entities/joinRequest";
import { VisibilityType } from "../src/core/entities/questionThread";
import { StatusType, Submission } from '../src/core/entities/submission';
import { SubmissionTypeORM } from '../src/infrastructure/database/data/data_models/submissionTypeorm';
import { SubmissionRepositoryTypeORM } from '../src/infrastructure/repositories/submissionRepositoryTypeORM';

export async function seedDatabase(): Promise<void> {
Expand All @@ -52,6 +50,8 @@ export async function seedDatabase(): Promise<void> {
const classIds: string[] = [];
const studentIds: string[] = [];
const assignments: { id: string, classId: string }[] = [];
// Assignments for which the startdate is in the past
const onGoingAssignments: { id: string, classId: string }[] = [];
const learningPathIds: string[] = [];

// Some random learningPath hruids from dwengo
Expand Down Expand Up @@ -136,10 +136,15 @@ export async function seedDatabase(): Promise<void> {
for (let i = 0; i < 3; i++) {
const learningPathId = faker.helpers.arrayElement(learningPaths);
learningPathIds.push(learningPathId)
// Choose a start date in the next 7 days
const startDate = faker.date.soon({ days: 7 });
// Deadline is sometime 1 to 14 days after startDate
const additionalDays = faker.number.int({ min: 1, max: 14 });

// Choose a start date between 24h ago and 24h in the future
const now = new Date();
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const startDate = faker.date.between({ from: past, to: future });

// Deadline is sometime 0 to 14 days after startDate
const additionalDays = faker.number.int({ min: 0, max: 14 });
const deadline = new Date(startDate.getTime() + additionalDays * 24 * 60 * 60 * 1000);
const name = faker.lorem.sentence(3); // Random name for the assignment
const extraInstructions = faker.lorem.sentence();
Expand All @@ -154,6 +159,9 @@ export async function seedDatabase(): Promise<void> {
);

const savedAssignment = await assignmentRep.create(assignment) as { id: string };
if (startDate < new Date()) {
onGoingAssignments.push({ id: savedAssignment.id, classId })
}
assignments.push({ id: savedAssignment.id, classId });
}
}
Expand Down Expand Up @@ -181,10 +189,10 @@ export async function seedDatabase(): Promise<void> {
}
}

// ── 6. Create submissions for Assignments ──
for (let i = 0 ; i < assignments.length; i++) {
const assignment: {id: string, classId: string} = assignments[i];
const students = await studentRep.getByClassId(assignment.classId);
// ── 7. Create submissions for Assignments that have started ──
for (let i = 0 ; i < onGoingAssignments.length; i++) {
const assignment: {id: string, classId: string} = onGoingAssignments[i];
const students = await userRep.getByClassId(assignment.classId);
const studentIds = students.map((s: any) => s.id);

for(const id of studentIds){
Expand All @@ -201,7 +209,7 @@ export async function seedDatabase(): Promise<void> {
}


// ── 7. Create Threads and Messages for Learning Steps ──
// ── 8. Create Threads and Messages for Learning Steps ──
const visibilityOptions = [
VisibilityType.PRIVATE,
VisibilityType.GROUP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,44 @@ export class DatasourceLearningPath extends DatasourceDwengo {
super(host, "learningPath");
}

public async getLearningPath(hruid: string, includeNodes: boolean, language?: string): Promise<LearningPath> {
// Fetch from dwengo
const response = await fetch(`${this.host}/api/${this.learningType}/search?hruid=${hruid}`);
private cache: LearningPathData[] | null = null;
private cacheTimestamp: number = 0;
private readonly CACHE_TTL = 12 * 60 * 60 * 1000; // 12u
protected async getAllLearningPathsCached(): Promise<LearningPathData[]> {
const now = Date.now();
if (this.cache && now - this.cacheTimestamp < this.CACHE_TTL) {
return this.cache;
}
const response = await fetch(`${this.host}/api/${this.learningType}/search?all=`);
if (!response.ok) {
throw {
code: ErrorCode.BAD_REQUEST,
message: `Error fetching from dwengo api: ${response.status}, ${response.statusText}`,
} as ApiError;
}
this.cache = await response.json();
this.cacheTimestamp = now;
return this.cache!;
}

const data = await response.json();
if (data.length === 0) {
public async getLearningPath(hruid: string, includeNodes: boolean, language?: string): Promise<LearningPath> {
// Fetch from dwengo or get from cache
const data = await this.getAllLearningPathsCached();
const matching = data.filter(path => path.hruid === hruid);
console.log(matching);

if (matching.length === 0) {
throw { code: ErrorCode.NOT_FOUND, message: `No learningPath exists with this hruid.` } as ApiError;
}

// Extract the right language from all the available paths
// (Dwengo API for some reason can't do this with &language=${language} in the query)
const learningPathObject: LearningPathData = language
? data.find((path: LearningPathData) => path.language === language)
: data[0];
const learningPathObject: LearningPathData | undefined = language
? matching.find((path: LearningPathData) => path.language === language)
: matching[0];
if (!learningPathObject) {
// Should not happen because language is checked beforehand, but just to be sure.
throw { code: ErrorCode.NOT_FOUND, message: `No learningPath exists with this hruid.` } as ApiError;
throw { code: ErrorCode.NOT_FOUND, message: `No learningPath exists in this language.` } as ApiError;
}
return LearningPath.fromObject(learningPathObject, includeNodes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("DatasourceLearningPath", () => {

const result = await datasource.getLearningPath("test-path", true, "nl");

expect(fetch).toHaveBeenCalledWith("https://dwengo.api/api/learningPath/search?hruid=test-path");
expect(fetch).toHaveBeenCalledWith("https://dwengo.api/api/learningPath/search?all=");
expect(result).toBeInstanceOf(LearningPath);
expect(result.toObject(true)).toEqual({
id: "1",
Expand Down Expand Up @@ -107,7 +107,7 @@ describe("DatasourceLearningPath", () => {

const result = await datasource.getLearningPath("test-path", true);

expect(fetch).toHaveBeenCalledWith("https://dwengo.api/api/learningPath/search?hruid=test-path");
expect(fetch).toHaveBeenCalledWith("https://dwengo.api/api/learningPath/search?all=");
expect(result).toBeInstanceOf(LearningPath);
expect(result.toObject(true)).toEqual({
id: "1",
Expand Down Expand Up @@ -173,7 +173,7 @@ describe("DatasourceLearningPath", () => {

await expect(datasource.getLearningPath("test-path", true, "nl")).rejects.toEqual({
code: ErrorCode.NOT_FOUND,
message: "No learningPath exists with this hruid.",
message: "No learningPath exists in this language.",
} as ApiError);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ header .links {
align-items: center;
justify-content: space-between;
margin-right: 5%;
gap: 5px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ <h2 i18n="@@joinRequestTitle">Join a class</h2>
formControlName="code"
required>
</mat-form-field>
<br>
<button mat-raised-button type="submit" [disabled]="requestForm.invalid" i18n="@@requestJoinSubmit">
<button color="primary" mat-raised-button type="submit" [disabled]="requestForm.invalid" i18n="@@requestJoinSubmit">
Request
</button>
</form>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.create-request {
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
}

form {
align-items: center;
flex-direction: column;
display: flex;
button {
width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.group-card {
cursor: pointer;
transition: background-color 0.3s;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="group-container">
<h3 class="title" i18n="@@groupMembersCount">
{{ data.members ? `Members: ${data.members.length}` : "No members"}}
</h3>
<div class="card-content">
@if (data.members) {
<mat-accordion class="list">
@for(member of data.members; track member.id) {
<app-mini-user [user]="member"></app-mini-user>
}
</mat-accordion>
}
<button style="padding: 10px;" (click)="exit()" color="primary" mat-fab>
<mat-icon>close</mat-icon>
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.group-container {
padding: 25px;
align-items: center;
justify-content: flex-start;
flex-direction: column;
display: flex;
height: 100%;
}

.card-content {
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
width: 100%;
}

.list {
padding: 25px;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupDialogComponent } from './group-dialog.component';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatExpansionModule } from '@angular/material/expansion';

describe('GroupDialogComponent', () => {
let component: GroupDialogComponent;
let fixture: ComponentFixture<GroupDialogComponent>;
let dialogRefSpy: jasmine.SpyObj<MatDialogRef<GroupDialogComponent>>;

const mockDataWithMembers = {
members: [
{ id: 1, firstName: 'Alice', familyName: 'test', email: "test@test.com", schoolName: "test" },
{ id: 2, name: 'Bob', familyName: 'test', email: "test@test.com", schoolName: "test" }
]
};

beforeEach(async () => {
dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['close']);

await TestBed.configureTestingModule({
imports: [
MatCardModule,
MatListModule,
MatButtonModule,
MatIconModule,
MatExpansionModule,
NoopAnimationsModule,
],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: mockDataWithMembers },
{ provide: MatDialogRef, useValue: dialogRefSpy }
]
}).compileComponents();

fixture = TestBed.createComponent(GroupDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create the component', () => {
expect(component).toBeTruthy();
});

it('should display the correct members count', () => {
component.data = mockDataWithMembers
fixture.detectChanges()
const title = fixture.debugElement.query(By.css('.title')).nativeElement;
expect(title.textContent).toContain('Members: 2');
});

it('should render mini-user components for each member', () => {
component.data = mockDataWithMembers
fixture.detectChanges()
const miniUsers = fixture.debugElement.queryAll(By.css('app-mini-user'));
expect(miniUsers.length).toBe(2);
});

it('should close dialog when exit button is clicked', () => {
const button = fixture.debugElement.query(By.css('button'));
button.triggerEventHandler('click', null);
expect(dialogRefSpy.close).toHaveBeenCalled();
});

it('should show "There are no users in this group" if no members', () => {
// Update data to no members
component.data.members = null;
fixture.detectChanges();

const noUsersMessage = fixture.debugElement.query(By.css('h3'));
expect(noUsersMessage.nativeElement.textContent.trim()).toBe('No members');
});
});
22 changes: 22 additions & 0 deletions frontend/src/app/components/group-dialog/group-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, inject } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MiniUserComponent } from '../mini-user/mini-user.component';
import { MatListModule } from '@angular/material/list';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatAccordion } from '@angular/material/expansion';

@Component({
selector: 'app-group-dialog',
imports: [MatCardModule, MiniUserComponent, MatListModule, MatButtonModule, MatIcon, MatAccordion],
templateUrl: './group-dialog.component.html',
styleUrl: './group-dialog.component.less'
})
export class GroupDialogComponent {
readonly dialogRef = inject(MatDialogRef<GroupDialogComponent>);
data = inject(MAT_DIALOG_DATA);
exit() {
this.dialogRef.close()
}
}
Loading