Skip to content
Closed

0.9.0 #182

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e1b8ae2
Add ts-node to run tests
jurei733 Jul 3, 2025
8febdd0
Fix frontend tests
jurei733 Jul 3, 2025
5be0134
Fix frontend test
jurei733 Jul 5, 2025
29a4511
Fix frontend linting
jurei733 Jul 6, 2025
2a846cb
Fix frontend linting
jurei733 Jul 6, 2025
88c98e2
Fix backend linting
jurei733 Jul 6, 2025
e4647b1
Refactor backend Service into specialized services
jurei733 Jul 6, 2025
d453ca8
Improve test results data rendering
jurei733 Jul 8, 2025
7bc1914
Improve test results data gathering performance
jurei733 Jul 8, 2025
ced9338
Scroll a block to 'start' in replay
jurei733 Jul 8, 2025
bc30f6d
Allow no anchor element replay
jurei733 Jul 8, 2025
8c36ab4
Update docker image for postgres
jurei733 Jul 8, 2025
6f2cf38
Merge pull request #176 from iqb-berlin/docker-image-postgres
jurei733 Jul 8, 2025
18f5dc6
Standardize feature-based organization with clear directory structure…
jurei733 Jul 8, 2025
6814f54
Make component models
jurei733 Jul 8, 2025
f3d195b
Move coding components in the components folder
jurei733 Jul 8, 2025
b7c6139
Add utils to feature modules
jurei733 Jul 8, 2025
7afe4ef
Move files to core
jurei733 Jul 8, 2025
be3689d
Move files to core
jurei733 Jul 8, 2025
a1452ea
Organize assets more systematically
jurei733 Jul 8, 2025
94bd576
Fix tests after merge
jurei733 Jul 8, 2025
a2d1e61
Scroll to the top if no anchor element is provided in the URL
jurei733 Jul 8, 2025
c88abff
Ensure validation occurs only when both the page ID and valid pages a…
jurei733 Jul 8, 2025
73bae9a
Optimize the retrieval of coding statistics
jurei733 Jul 8, 2025
a6655d2
Merge pull request #177 from iqb-berlin/0.8.5
jurei733 Jul 9, 2025
9fd1be6
Coding test persons in jobs
jurei733 Jul 9, 2025
6209a16
Implement Caching mechanisms for coding schemes and test files in the…
jurei733 Jul 9, 2025
5b4519c
Use more height for unit-player
jurei733 Jul 9, 2025
fdb10aa
Merge pull request #178 from iqb-berlin/0.8.6
jurei733 Jul 9, 2025
e4e191a
Check for parameters, limit IDs in IN statements to 1000, and verify …
jurei733 Jul 10, 2025
3d28bea
Improve results validation
jurei733 Jul 10, 2025
4868c16
Give player fixed height
jurei733 Jul 10, 2025
aea7819
Edit keycloak settings on server app
jurei733 Jul 11, 2025
0d60401
Customize database config
jurei733 Jul 13, 2025
2e98c71
Add indexes to db for faster bulk inserts
jurei733 Jul 13, 2025
a2a570a
Set version to 0.9.0
jurei733 Jul 13, 2025
bba1342
Lower postgres performance settings
jurei733 Jul 13, 2025
f1b77f3
Include standard postgres config
jurei733 Jul 13, 2025
f90d908
Fix postgres dockerfile
jurei733 Jul 13, 2025
f001736
Deactivate customised postgres conf
jurei733 Jul 13, 2025
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
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ For production environments, you can use the file `docker-compose.coding-box.pro

---

## Key Features


---

## Useful Scripts

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
}
},
"lint": {
"executor": "@nx/linter:eslint",
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
],
Expand Down
24 changes: 15 additions & 9 deletions apps/backend/src/app/admin/logo/logo.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { AppLogoDto } from '../../../../../../api-dto/app-logo-dto';
@Controller('admin/logo')
@ApiTags('admin')
export class LogoController {
LOGO_PATH = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'logo');
LOGO_PATH = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'images');
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
MAX_FILE_SIZE = 4 * 1024 * 1024; // 4MB

Expand All @@ -42,7 +42,7 @@ export class LogoController {
FileInterceptor('logo', {
storage: diskStorage({
destination: (req, file, cb) => {
const uploadPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets');
const uploadPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'images');
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
Expand Down Expand Up @@ -93,7 +93,7 @@ export class LogoController {
try {
// Always return the consistent path to the uploaded file
// This ensures the path matches the actual saved filename (logo + extension)
return { path: `assets/logo${path.extname(file.originalname)}` };
return { path: `assets/images/logo${path.extname(file.originalname)}` };
} catch (error) {
throw new InternalServerErrorException('Failed to upload logo');
}
Expand All @@ -106,8 +106,8 @@ export class LogoController {
@ApiOkResponse({ description: 'Logo deleted successfully', type: Boolean })
async deleteLogo(): Promise<{ success: boolean }> {
try {
// Find all files starting with 'logo' in the assets directory
const assetsDir = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets');
// Find all files starting with 'logo' in the images directory
const assetsDir = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'images');
const files = fs.readdirSync(assetsDir);

let deleted = false;
Expand All @@ -119,7 +119,7 @@ export class LogoController {
}

// Delete logo settings file if it exists
const settingsPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'logo-settings.json');
const settingsPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'data', 'logo-settings.json');
if (fs.existsSync(settingsPath)) {
fs.unlinkSync(settingsPath);
}
Expand All @@ -138,7 +138,13 @@ export class LogoController {
@ApiOkResponse({ description: 'Logo settings saved successfully', type: Boolean })
async saveLogoSettings(@Body() logoSettings: AppLogoDto): Promise<{ success: boolean }> {
try {
const settingsPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'logo-settings.json');
const dataDir = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'data');
const settingsPath = path.join(dataDir, 'logo-settings.json');

// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}

// Save the settings to a file
fs.writeFileSync(settingsPath, JSON.stringify(logoSettings, null, 2));
Expand All @@ -156,7 +162,7 @@ export class LogoController {
@ApiOkResponse({ description: 'Logo settings retrieved successfully', type: AppLogoDto })
async getLogoSettings(): Promise<AppLogoDto> {
try {
const settingsPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'logo-settings.json');
const settingsPath = path.join(process.cwd(), 'apps', 'frontend', 'src', 'assets', 'data', 'logo-settings.json');

// Check if settings file exists
if (fs.existsSync(settingsPath)) {
Expand All @@ -167,7 +173,7 @@ export class LogoController {

// Return default settings if file doesn't exist
return {
data: 'assets/IQB-LogoA.png',
data: 'assets/images/IQB-LogoA.png',
alt: 'Zur Startseite',
bodyBackground: 'linear-gradient(180deg, rgba(7,70,94,1) 0%, rgba(6,112,123,1) 24%, rgba(1,192,229,1) 85%)',
boxBackground: 'lightgray'
Expand Down
136 changes: 135 additions & 1 deletion apps/backend/src/app/admin/workspace/workspace-coding.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
Controller,
Get, Query, Res, UseGuards
Get, Param, Query, Res, UseGuards
} from '@nestjs/common';
import {
ApiOkResponse,
Expand Down Expand Up @@ -173,4 +173,138 @@ export class WorkspaceCodingController {
async getCodingStatistics(@WorkspaceId() workspace_id: number): Promise<CodingStatistics> {
return this.workspaceCodingService.getCodingStatistics(workspace_id);
}

@Get(':workspace_id/coding/job/:jobId')
@UseGuards(JwtAuthGuard, WorkspaceGuard)
@ApiTags('coding')
@ApiParam({ name: 'workspace_id', type: Number })
@ApiParam({ name: 'jobId', type: String, description: 'ID of the background job' })
@ApiOkResponse({
description: 'Job status retrieved successfully.',
schema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'processing', 'completed', 'failed', 'cancelled'],
description: 'Current status of the job'
},
progress: {
type: 'number',
description: 'Progress percentage (0-100)'
},
result: {
type: 'object',
description: 'Result of the job (only available when status is completed)',
properties: {
totalResponses: { type: 'number' },
statusCounts: {
type: 'object',
additionalProperties: { type: 'number' }
}
}
},
error: {
type: 'string',
description: 'Error message (only available when status is failed)'
}
}
}
})
async getJobStatus(@Param('jobId') jobId: string): Promise<{ status: string; progress: number; result?: CodingStatistics; error?: string } | { error: string }> {
const status = this.workspaceCodingService.getJobStatus(jobId);
if (!status) {
return { error: `Job with ID ${jobId} not found` };
}
return status;
}

@Get(':workspace_id/coding/job/:jobId/cancel')
@UseGuards(JwtAuthGuard, WorkspaceGuard)
@ApiTags('coding')
@ApiParam({ name: 'workspace_id', type: Number })
@ApiParam({ name: 'jobId', type: String, description: 'ID of the background job to cancel' })
@ApiOkResponse({
description: 'Job cancellation request processed.',
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: 'Whether the cancellation request was successful'
},
message: {
type: 'string',
description: 'Message describing the result of the cancellation request'
}
}
}
})
async cancelJob(@Param('jobId') jobId: string): Promise<{ success: boolean; message: string }> {
return this.workspaceCodingService.cancelJob(jobId);
}

@Get(':workspace_id/coding/jobs')
@UseGuards(JwtAuthGuard, WorkspaceGuard)
@ApiTags('coding')
@ApiParam({ name: 'workspace_id', type: Number })
@ApiOkResponse({
description: 'List of all jobs retrieved successfully.',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
jobId: {
type: 'string',
description: 'Unique identifier for the job'
},
status: {
type: 'string',
enum: ['pending', 'processing', 'completed', 'failed', 'cancelled'],
description: 'Current status of the job'
},
progress: {
type: 'number',
description: 'Progress percentage (0-100)'
},
result: {
type: 'object',
description: 'Result of the job (only available when status is completed)',
properties: {
totalResponses: { type: 'number' },
statusCounts: {
type: 'object',
additionalProperties: { type: 'number' }
}
}
},
error: {
type: 'string',
description: 'Error message (only available when status is failed)'
},
workspaceId: {
type: 'number',
description: 'ID of the workspace the job belongs to'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Date and time when the job was created'
}
}
}
}
})
async getAllJobs(@WorkspaceId() workspace_id: number): Promise<{
jobId: string;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
progress: number;
result?: CodingStatistics;
error?: string;
workspaceId?: number;
createdAt?: Date;
}[]> {
return this.workspaceCodingService.getAllJobs(workspace_id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AuthService } from '../../auth/service/auth.service';
import { UsersService } from '../../database/services/users.service';
import { TestcenterService } from '../../database/services/testcenter.service';
import { UploadResultsService } from '../../database/services/upload-results.service'; // ggf. anpassen, falls anderer Pfad
import { WorkspaceCoreService } from '../../database/services/workspace-core.service';

describe('WorkspaceController', () => {
let controller: WorkspaceController;
Expand All @@ -28,6 +29,10 @@ describe('WorkspaceController', () => {
{
provide: UploadResultsService,
useValue: createMock<UploadResultsService>() // Mock-Implementierung für UploadResultsService
},
{
provide: WorkspaceCoreService,
useValue: createMock<WorkspaceCoreService>()
}
]
}).compile();
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/app/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppController } from './app.controller';
import { AuthService } from './auth/service/auth.service';
import { UsersService } from './database/services/users.service';
import { TestcenterService } from './database/services/testcenter.service';
import { WorkspaceUsersService } from './database/services/workspace-users.service';

describe('AppController', () => {
beforeEach(async () => {
Expand All @@ -21,6 +22,10 @@ describe('AppController', () => {
{
provide: TestcenterService,
useValue: createMock<TestcenterService>()
},
{
provide: WorkspaceUsersService,
useValue: createMock<WorkspaceUsersService>()
}
]
}).compile();
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/app/database/services/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export class PersonService {
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private extractVariablesFromSubforms(subforms: any[]): Set<string> {
const variables = new Set<string>();
subforms.forEach(subform => subform.responses.forEach(response => variables.add(response.id))
Expand Down Expand Up @@ -654,6 +655,7 @@ export class PersonService {

async saveSubformResponsesForUnit(
savedUnit: Unit,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subforms: any[]
): Promise<{ success: boolean; saved: number; skipped: number }> {
try {
Expand Down Expand Up @@ -765,7 +767,9 @@ export class PersonService {

async processPersonLogs(
persons: Person[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unitLogs: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
bookletLogs: any,
overwriteExistingLogs: boolean = true
): Promise<{
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/app/database/services/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,8 @@ export interface CodingStatistics {
[key: string]: number;
};
}

export interface CodingStatisticsWithJob extends CodingStatistics {
jobId?: string;
message?: string;
}
28 changes: 27 additions & 1 deletion apps/backend/src/app/database/services/unit-tag.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, In } from 'typeorm';
import { UnitTag } from '../entities/unitTag.entity';
import { Unit } from '../entities/unit.entity';
import { CreateUnitTagDto } from '../../../../../../api-dto/unit-tags/create-unit-tag.dto';
Expand Down Expand Up @@ -82,6 +82,32 @@ export class UnitTagService {
}));
}

/**
* Find all tags for multiple units in a single query
* @param unitIds Array of unit IDs
* @returns An array of tags for all specified units
*/
async findAllByUnitIds(unitIds: number[]): Promise<UnitTagDto[]> {
if (!unitIds || unitIds.length === 0) {
return [];
}

// Find all tags for the units in a single query
const tags = await this.unitTagRepository.find({
where: { unitId: In(unitIds) },
order: { createdAt: 'DESC' }
});

// Return the DTOs
return tags.map(tag => ({
id: tag.id,
unitId: tag.unitId,
tag: tag.tag,
color: tag.color,
createdAt: tag.createdAt
}));
}

/**
* Find a tag by ID
* @param id The ID of the tag
Expand Down
Loading