diff --git a/frontend/src/app/assignment/assignment.module.ts b/frontend/src/app/assignment/assignment.module.ts index 02d90dad7..eca73fd6f 100644 --- a/frontend/src/app/assignment/assignment.module.ts +++ b/frontend/src/app/assignment/assignment.module.ts @@ -1,5 +1,5 @@ import {CommonModule} from '@angular/common'; -import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; @@ -19,6 +19,19 @@ import {OverviewComponent} from './pages/overview/overview.component'; import {SettingsComponent} from './pages/settings/settings.component'; import {TokenModalComponent} from './pages/token-modal/token-modal.component'; import {ConfigService} from './services/config.service'; +import {TokenInterceptor} from "./services/token.interceptor"; +import {AssignmentService} from "./services/assignment.service"; +import {TokenService} from "./services/token.service"; +import {SolutionService} from "./services/solution.service"; +import {TelemetryService} from "./services/telemetry.service"; +import {CourseService} from "./services/course.service"; +import {SelectionService} from "./services/selection.service"; +import {SolutionContainerService} from "./services/solution-container.service"; +import {TaskService} from "./services/task.service"; +import {SubmitService} from "./modules/assignment/submit.service"; +import {AssigneeService} from "./services/assignee.service"; +import {EvaluationService} from "./services/evaluation.service"; +import {EmbeddingService} from "./services/embedding.service"; @NgModule({ declarations: [ @@ -43,7 +56,24 @@ import {ConfigService} from './services/config.service'; ModalModule, ], providers: [ + { + provide: HTTP_INTERCEPTORS, + multi: true, + useClass: TokenInterceptor, + }, + TokenService, ConfigService, + AssignmentService, + SolutionService, + TelemetryService, + CourseService, + SelectionService, + SolutionContainerService, + TaskService, + SubmitService, + AssigneeService, + EvaluationService, + EmbeddingService, ], }) export class AssignmentModule { diff --git a/frontend/src/app/assignment/modules/assignment/assignment.module.ts b/frontend/src/app/assignment/modules/assignment/assignment.module.ts index a9869d85c..48be25ee6 100644 --- a/frontend/src/app/assignment/modules/assignment/assignment.module.ts +++ b/frontend/src/app/assignment/modules/assignment/assignment.module.ts @@ -24,6 +24,8 @@ import {StatisticsBlockComponent} from './statistics-block/statistics-block.comp import {StatisticsComponent} from './statistics/statistics.component'; import {SubmitModalComponent} from './submit-modal/submit-modal.component'; import {AssignmentTasksComponent} from './tasks/tasks.component'; +import {StatisticsService} from "./statistics.service"; +import {SubmitService} from "./submit.service"; @NgModule({ declarations: [ @@ -53,6 +55,10 @@ import {AssignmentTasksComponent} from './tasks/tasks.component'; RouteTabsModule, ModalModule, ], + providers: [ + StatisticsService, + SubmitService, + ], }) export class AssignmentModule { } diff --git a/frontend/src/app/assignment/modules/assignment/solution-table/solution-table.component.ts b/frontend/src/app/assignment/modules/assignment/solution-table/solution-table.component.ts index a0285c9ba..1ba9d34dd 100644 --- a/frontend/src/app/assignment/modules/assignment/solution-table/solution-table.component.ts +++ b/frontend/src/app/assignment/modules/assignment/solution-table/solution-table.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit, TrackByFunction} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {ToastService} from '@mean-stream/ngbx'; import {ClipboardService} from 'ngx-clipboard'; -import {BehaviorSubject, combineLatest, firstValueFrom, forkJoin, Observable} from 'rxjs'; +import {BehaviorSubject, combineLatest, forkJoin, Observable} from 'rxjs'; import {debounceTime, distinctUntilChanged, map, switchMap, tap} from 'rxjs/operators'; import {Assignee} from '../../../model/assignee'; import Assignment, {ReadAssignmentDto} from '../../../model/assignment'; @@ -15,6 +15,8 @@ import {TaskService} from '../../../services/task.service'; import {TelemetryService} from '../../../services/telemetry.service'; import {SubmitService} from "../submit.service"; import {UserService} from "../../../../user/user.service"; +import {AssigneeService} from "../../../services/assignee.service"; +import {EvaluationService} from "../../../services/evaluation.service"; type SearchKey = keyof AuthorInfo | 'assignee'; const searchKeys: readonly SearchKey[] = [ @@ -56,6 +58,8 @@ export class SolutionTableComponent implements OnInit { constructor( private assignmentService: AssignmentService, private solutionService: SolutionService, + private assigneeService: AssigneeService, + private evaluationService: EvaluationService, private solutionContainerService: SolutionContainerService, private configService: ConfigService, private router: Router, @@ -78,7 +82,7 @@ export class SolutionTableComponent implements OnInit { }); this.activatedRoute.params.pipe( - switchMap(({aid}) => this.solutionService.getAssignees(aid)), + switchMap(({aid}) => this.assigneeService.getAssignees(aid)), ).subscribe(assignees => { this.assignees = {}; const names = new Set(); @@ -91,8 +95,8 @@ export class SolutionTableComponent implements OnInit { this.activatedRoute.params.pipe( switchMap(({aid}) => forkJoin([ - this.solutionService.getEvaluationValues(aid, 'solution', {codeSearch: false}), - this.solutionService.getEvaluationValues(aid, 'solution', {codeSearch: true}), + this.evaluationService.distinctValues(aid, 'solution', {codeSearch: false}), + this.evaluationService.distinctValues(aid, 'solution', {codeSearch: true}), ])), ).subscribe(([manual, codeSearch]) => { this.evaluated = {}; diff --git a/frontend/src/app/assignment/modules/assignment/statistics.service.ts b/frontend/src/app/assignment/modules/assignment/statistics.service.ts index 4932ab46f..bc8f72886 100644 --- a/frontend/src/app/assignment/modules/assignment/statistics.service.ts +++ b/frontend/src/app/assignment/modules/assignment/statistics.service.ts @@ -51,9 +51,7 @@ export interface AssignmentStatistics { tasks: TaskStatistics[]; } -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class StatisticsService { constructor( private http: HttpClient, diff --git a/frontend/src/app/assignment/modules/assignment/submit.service.ts b/frontend/src/app/assignment/modules/assignment/submit.service.ts index d0acaa0fb..8a5febbf8 100644 --- a/frontend/src/app/assignment/modules/assignment/submit.service.ts +++ b/frontend/src/app/assignment/modules/assignment/submit.service.ts @@ -2,14 +2,13 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {firstValueFrom} from 'rxjs'; import {environment} from '../../../../environments/environment'; -import {UserService} from '../../../user/user.service'; import {ReadAssignmentDto} from '../../model/assignment'; import {Evaluation, Snippet} from '../../model/evaluation'; import Solution from '../../model/solution'; import Task from '../../model/task'; -import {AssignmentService} from '../../services/assignment.service'; import {SolutionService} from '../../services/solution.service'; import {TaskService} from '../../services/task.service'; +import {EvaluationService} from "../../services/evaluation.service"; export interface Issue { number: number; @@ -20,16 +19,13 @@ export interface Issue { export type IssueDto = Omit; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class SubmitService { constructor( - private assignmentService: AssignmentService, + private evaluationService: EvaluationService, private solutionService: SolutionService, private taskService: TaskService, - private userService: UserService, private http: HttpClient, ) { } @@ -73,7 +69,7 @@ export class SubmitService { } private async renderTasks(assignment: ReadAssignmentDto, solution: Solution) { - const evaluations = await firstValueFrom(this.solutionService.getEvaluations(assignment._id, solution._id!)); + const evaluations = await firstValueFrom(this.evaluationService.findAll(assignment._id, solution._id!)); const evaluationRecord: Record = {}; for (let evaluation of evaluations) { evaluationRecord[evaluation.task] = evaluation; diff --git a/frontend/src/app/assignment/modules/edit-assignment/edit-assignment.module.ts b/frontend/src/app/assignment/modules/edit-assignment/edit-assignment.module.ts index 4caa4cc39..d2afd0940 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/edit-assignment.module.ts +++ b/frontend/src/app/assignment/modules/edit-assignment/edit-assignment.module.ts @@ -17,6 +17,7 @@ import {PreviewComponent} from './preview/preview.component'; import {SampleComponent} from './sample/sample.component'; import {TasksComponent} from './tasks/tasks.component'; import {TemplateComponent} from './template/template.component'; +import {TaskMarkdownService} from "./task-markdown.service"; @NgModule({ @@ -42,6 +43,9 @@ import {TemplateComponent} from './template/template.component'; RouteTabsModule, ModalModule, ], + providers: [ + TaskMarkdownService, + ], }) export class EditAssignmentModule { } diff --git a/frontend/src/app/assignment/modules/edit-assignment/task-markdown.service.ts b/frontend/src/app/assignment/modules/edit-assignment/task-markdown.service.ts index 323561f02..a2f3636c8 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/task-markdown.service.ts +++ b/frontend/src/app/assignment/modules/edit-assignment/task-markdown.service.ts @@ -3,9 +3,7 @@ import {extractTaskItem, TASK_ITEM_PATTERN} from '../../../../modes/task-list-co import Task from '../../model/task'; import {TaskService} from '../../services/task.service'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class TaskMarkdownService { constructor( private taskService: TaskService, diff --git a/frontend/src/app/assignment/modules/import/import-embeddings/import-embeddings.component.ts b/frontend/src/app/assignment/modules/import/import-embeddings/import-embeddings.component.ts index 50b94fdc3..77e4c6535 100644 --- a/frontend/src/app/assignment/modules/import/import-embeddings/import-embeddings.component.ts +++ b/frontend/src/app/assignment/modules/import/import-embeddings/import-embeddings.component.ts @@ -1,8 +1,8 @@ import {Component, OnInit} from '@angular/core'; import {EstimatedCosts} from "../../../model/solution"; -import {SolutionService} from "../../../services/solution.service"; import {ActivatedRoute} from "@angular/router"; -import {tap} from "rxjs/operators"; +import {switchMap, tap} from "rxjs/operators"; +import {EmbeddingService} from "../../../services/embedding.service"; @Component({ selector: 'app-import-embeddings', @@ -14,18 +14,20 @@ export class ImportEmbeddingsComponent implements OnInit { finalCosts?: EstimatedCosts; constructor( - private solutionService: SolutionService, + private embeddingService: EmbeddingService, private route: ActivatedRoute, ) { } ngOnInit() { - this.solutionService.importEmbeddings(this.route.snapshot.params.aid, true).subscribe(costs => this.estimatedCosts = costs); + this.route.params.pipe( + switchMap(({aid}) => this.embeddingService.import(aid, true)), + ).subscribe(costs => this.estimatedCosts = costs); } import() { const assignmentId = this.route.snapshot.params.aid; - return this.solutionService.importEmbeddings(assignmentId).pipe( + return this.embeddingService.import(assignmentId).pipe( tap(result => this.finalCosts = result), ); } diff --git a/frontend/src/app/assignment/modules/shared/assignee-input/assignee-input.component.ts b/frontend/src/app/assignment/modules/shared/assignee-input/assignee-input.component.ts index 4d15b8794..f779ab332 100644 --- a/frontend/src/app/assignment/modules/shared/assignee-input/assignee-input.component.ts +++ b/frontend/src/app/assignment/modules/shared/assignee-input/assignee-input.component.ts @@ -3,7 +3,7 @@ import {ToastService} from '@mean-stream/ngbx'; import {Observable} from 'rxjs'; import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators'; import {Assignee} from '../../../model/assignee'; -import {SolutionService} from '../../../services/solution.service'; +import {AssigneeService} from "../../../services/assignee.service"; @Component({ selector: 'app-assignee-input', @@ -29,14 +29,14 @@ export class AssigneeInputComponent { ); constructor( - private solutionService: SolutionService, + private assigneeService: AssigneeService, private toastService: ToastService, ) { } save(): void { this.saving = true; - this.solutionService.setAssignee(this.assignment, this.solution, this.assignee).subscribe(result => { + this.assigneeService.setAssignee(this.assignment, this.solution, this.assignee).subscribe(result => { this.saving = false; this.saved.next(result); this.assigneeChange.next(result.assignee); diff --git a/frontend/src/app/assignment/modules/shared/task-list/task-list.component.ts b/frontend/src/app/assignment/modules/shared/task-list/task-list.component.ts index 1fc31ac12..8b13af263 100644 --- a/frontend/src/app/assignment/modules/shared/task-list/task-list.component.ts +++ b/frontend/src/app/assignment/modules/shared/task-list/task-list.component.ts @@ -7,6 +7,7 @@ import Task from '../../../model/task'; import {ConfigService} from '../../../services/config.service'; import {SolutionService} from '../../../services/solution.service'; import {TelemetryService} from '../../../services/telemetry.service'; +import {EvaluationService} from "../../../services/evaluation.service"; @Component({ selector: 'app-task-list', @@ -21,6 +22,7 @@ export class TaskListComponent { constructor( private telemetryService: TelemetryService, private solutionService: SolutionService, + private evaluationService: EvaluationService, private configService: ConfigService, private toastService: ToastService, private route: ActivatedRoute, @@ -38,10 +40,10 @@ export class TaskListComponent { givePoints(task: Task, points: number) { const {aid, sid} = this.route.snapshot.params; - this.solutionService.getEvaluationByTask(aid, sid, task._id).pipe( + this.evaluationService.findByTask(aid, sid, task._id).pipe( switchMap(evaluation => evaluation ? - this.solutionService.updateEvaluation(aid, sid, evaluation._id, {points}) : - this.solutionService.createEvaluation(aid, sid, { + this.evaluationService.update(aid, sid, evaluation._id, {points}) : + this.evaluationService.create(aid, sid, { task: task._id, points, author: this.configService.get('name'), diff --git a/frontend/src/app/assignment/modules/solution/comment-list/comment-list.component.ts b/frontend/src/app/assignment/modules/solution/comment-list/comment-list.component.ts index 30eeec740..2400657af 100644 --- a/frontend/src/app/assignment/modules/solution/comment-list/comment-list.component.ts +++ b/frontend/src/app/assignment/modules/solution/comment-list/comment-list.component.ts @@ -2,11 +2,12 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {ToastService} from '@mean-stream/ngbx'; import {Subscription} from 'rxjs'; -import {mapTo, switchMap, take, tap} from 'rxjs/operators'; +import {mapTo, switchMap, tap} from 'rxjs/operators'; import {UserService} from '../../../../user/user.service'; import Comment from '../../../model/comment'; import {ConfigService} from '../../../services/config.service'; import {SolutionService} from '../../../services/solution.service'; +import {CommentService} from "../comment.service"; @Component({ selector: 'app-comment-list', @@ -27,6 +28,7 @@ export class CommentListComponent implements OnInit, OnDestroy { constructor( private userService: UserService, private solutionService: SolutionService, + private commentService: CommentService, private configService: ConfigService, private toastService: ToastService, private route: ActivatedRoute, @@ -36,11 +38,11 @@ export class CommentListComponent implements OnInit, OnDestroy { ngOnInit(): void { const eventSub = this.route.params.pipe( tap(({sid}) => this.loadCommentDraft(sid)), - switchMap(params => this.solutionService.getComments(params.aid, params.sid).pipe( + switchMap(params => this.commentService.findAll(params.aid, params.sid).pipe( tap(comments => this.comments = comments), mapTo(params), )), - switchMap(({aid, sid}) => this.solutionService.streamComments(aid, sid)), + switchMap(({aid, sid}) => this.commentService.stream(aid, sid)), ).subscribe(({event, comment}) => { this.safeApply(comment._id!, event === 'deleted' ? undefined : comment); }); @@ -71,14 +73,14 @@ export class CommentListComponent implements OnInit, OnDestroy { loadCommentDraft(solution: string): void { this.commentName = this.configService.get('name'); this.commentEmail = this.configService.get('email'); - this.commentBody = this.solutionService.getCommentDraft(solution) || ''; + this.commentBody = this.commentService.getDraft(solution) || ''; } saveCommentDraft(): void { const solution = this.route.snapshot.params.sid; this.configService.set('name', this.commentName); this.configService.set('email', this.commentEmail); - this.solutionService.setCommentDraft(solution, this.commentBody); + this.commentService.setDraft(solution, this.commentBody); } submitComment(): void { @@ -92,7 +94,7 @@ export class CommentListComponent implements OnInit, OnDestroy { email: this.commentEmail, body: this.commentBody, }; - this.solutionService.postComment(aid, sid, comment).subscribe(result => { + this.commentService.post(aid, sid, comment).subscribe(result => { this.safeApply(result._id!, result); this.commentBody = ''; this.saveCommentDraft(); @@ -109,7 +111,7 @@ export class CommentListComponent implements OnInit, OnDestroy { } const {sid, aid} = this.route.snapshot.params; - this.solutionService.deleteComment(aid, sid, comment._id!).subscribe(result => { + this.commentService.delete(aid, sid, comment._id!).subscribe(result => { this.safeApply(result._id!, undefined); this.toastService.warn('Comment', 'Successfully deleted comment'); }, error => { diff --git a/frontend/src/app/assignment/modules/solution/comment.service.ts b/frontend/src/app/assignment/modules/solution/comment.service.ts new file mode 100644 index 000000000..55cfeb1ff --- /dev/null +++ b/frontend/src/app/assignment/modules/solution/comment.service.ts @@ -0,0 +1,43 @@ +import {Injectable} from "@angular/core"; +import {Observable} from "rxjs"; +import Comment from "../../model/comment"; +import {environment} from "../../../../environments/environment"; +import {observeSSE} from "../../services/sse-helper"; +import {HttpClient} from "@angular/common/http"; +import {TokenService} from "../../services/token.service"; +import {StorageService} from "../../../services/storage.service"; + +@Injectable() +export class CommentService { + constructor( + private http: HttpClient, + private tokenService: TokenService, + private storageService: StorageService, + ) { + } + + getDraft(solution: string): string | null { + return this.storageService.get(`commentDraft/${solution}`); + } + + setDraft(solution: string, draft: string | null) { + this.storageService.set(`commentDraft/${solution}`, draft); + } + + findAll(assignment: string, solution: string): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments`); + } + + post(assignment: string, solution: string, comment: Comment): Observable { + return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments`, comment); + } + + delete(assignment: string, solution: string, comment: string): Observable { + return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments/${comment}`); + } + + stream(assignment: string, solution: string): Observable<{ event: string, comment: Comment }> { + const token = this.tokenService.getSolutionToken(assignment, solution) || this.tokenService.getAssignmentToken(assignment); + return observeSSE(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments/events?token=${token}`); + } +} diff --git a/frontend/src/app/assignment/modules/solution/delete-modal/delete-modal.component.ts b/frontend/src/app/assignment/modules/solution/delete-modal/delete-modal.component.ts index e0b3dd7ab..94f38f48d 100644 --- a/frontend/src/app/assignment/modules/solution/delete-modal/delete-modal.component.ts +++ b/frontend/src/app/assignment/modules/solution/delete-modal/delete-modal.component.ts @@ -5,6 +5,8 @@ import {switchMap} from 'rxjs/operators'; import Solution from '../../../model/solution'; import {SolutionService} from '../../../services/solution.service'; import {SolutionNamePipe} from '../../shared/pipes/solution-name.pipe'; +import {EvaluationService} from "../../../services/evaluation.service"; +import {CommentService} from "../comment.service"; @Component({ selector: 'app-delete-modal', @@ -22,6 +24,8 @@ export class DeleteModalComponent implements OnInit { constructor( public route: ActivatedRoute, private solutionService: SolutionService, + private evaluationService: EvaluationService, + private commentService: CommentService, private toastService: ToastService, ) { } @@ -35,11 +39,11 @@ export class DeleteModalComponent implements OnInit { }); this.route.params.pipe( - switchMap(({aid, sid}) => this.solutionService.getComments(aid, sid)), + switchMap(({aid, sid}) => this.commentService.findAll(aid, sid)), ).subscribe(comments => this.comments = comments.length); this.route.params.pipe( - switchMap(({aid, sid}) => this.solutionService.getEvaluations(aid, sid)), + switchMap(({aid, sid}) => this.evaluationService.findAll(aid, sid)), ).subscribe(evaluations => this.evaluations = evaluations.length); } diff --git a/frontend/src/app/assignment/modules/solution/evaluation-form/evaluation-form.component.ts b/frontend/src/app/assignment/modules/solution/evaluation-form/evaluation-form.component.ts index ad41427b6..18cd50bf9 100644 --- a/frontend/src/app/assignment/modules/solution/evaluation-form/evaluation-form.component.ts +++ b/frontend/src/app/assignment/modules/solution/evaluation-form/evaluation-form.component.ts @@ -4,7 +4,7 @@ import {switchMap} from 'rxjs/operators'; import {CreateEvaluationDto, RemarkDto} from '../../../model/evaluation'; import Task from '../../../model/task'; import {ConfigService} from '../../../services/config.service'; -import {SolutionService} from '../../../services/solution.service'; +import {EvaluationService} from "../../../services/evaluation.service"; @Component({ selector: 'app-evaluation-form', @@ -23,7 +23,7 @@ export class EvaluationFormComponent implements OnInit, OnChanges { remarks: RemarkDto[] = []; constructor( - private solutionService: SolutionService, + private evaluationService: EvaluationService, private configService: ConfigService, private route: ActivatedRoute, ) { @@ -31,7 +31,7 @@ export class EvaluationFormComponent implements OnInit, OnChanges { ngOnInit(): void { this.route.params.pipe( - switchMap(({aid, task}) => this.solutionService.getEvaluationRemarks(aid, {task})), + switchMap(({aid, task}) => this.evaluationService.distinctRemarks(aid, {task})), ).subscribe(remarks => this.remarks = remarks); } diff --git a/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts b/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts index ead1054a3..cedd3629d 100644 --- a/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts +++ b/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts @@ -1,5 +1,5 @@ import {Component, HostListener, OnDestroy, OnInit, ViewChild} from '@angular/core'; -import {ActivatedRoute, Route, Router} from '@angular/router'; +import {ActivatedRoute, Router} from '@angular/router'; import {ModalComponent, ToastService} from '@mean-stream/ngbx'; import {EMPTY, merge, of, Subject, Subscription} from 'rxjs'; import {debounceTime, distinctUntilChanged, filter, map, share, switchMap, tap} from 'rxjs/operators'; @@ -13,6 +13,8 @@ import {SolutionService} from '../../../services/solution.service'; import {TaskService} from '../../../services/task.service'; import {TelemetryService} from '../../../services/telemetry.service'; import {SelectionService} from '../../../services/selection.service'; +import {EvaluationService} from "../../../services/evaluation.service"; +import {EmbeddingService} from "../../../services/embedding.service"; export const selectionComment = '(fulibFeedback Selection)'; @@ -59,6 +61,8 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { private configService: ConfigService, private toastService: ToastService, private telemetryService: TelemetryService, + private evaluationService: EvaluationService, + private embeddingService: EmbeddingService, public route: ActivatedRoute, private router: Router, ) { @@ -75,7 +79,7 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { }); const evaluation$ = this.route.params.pipe( - switchMap(({aid, sid, task}) => this.solutionService.getEvaluationByTask(aid, sid, task)), + switchMap(({aid, sid, task}) => this.evaluationService.findByTask(aid, sid, task)), share(), ); @@ -90,7 +94,7 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { this.subscriptions.add(evaluation$.pipe( switchMap(evaluation => { const origin = evaluation?.codeSearch?.origin; - return origin ? this.solutionService.getEvaluation(evaluation.assignment, undefined, origin) : of(undefined); + return origin ? this.evaluationService.findOne(evaluation.assignment, undefined, origin) : of(undefined); }), tap(originEvaluation => this.originEvaluation = originEvaluation), switchMap(originEvaluation => originEvaluation ? this.solutionService.get(originEvaluation.assignment, originEvaluation.solution) : of(undefined)), @@ -98,18 +102,18 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { ).subscribe()); this.subscriptions.add(evaluation$.pipe( - switchMap(evaluation => evaluation ? this.solutionService.getEvaluationValues(evaluation.assignment, 'solution', { + switchMap(evaluation => evaluation ? this.evaluationService.distinctValues(evaluation.assignment, 'solution', { origin: evaluation._id, task: evaluation.task, }) : EMPTY), ).subscribe(solutionIds => this.derivedSolutionCount = solutionIds.length)); this.route.params.pipe( - switchMap(({aid, task}) => this.solutionService.getEvaluationValues(aid, 'snippets.comment', {task})), + switchMap(({aid, task}) => this.evaluationService.distinctValues(aid, 'snippets.comment', {task})), ).subscribe(comments => this.comments = comments); this.route.params.pipe( - switchMap(({aid, sid, task}) => this.solutionService.getEmbeddingSnippets(aid, sid, task)), + switchMap(({aid, sid, task}) => this.embeddingService.findTaskRelatedSnippets(aid, sid, task)), ).subscribe(snippets => this.embeddingSnippets = snippets); const selection$ = this.route.params.pipe( @@ -200,8 +204,8 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { }).subscribe(); const op = this.evaluation - ? this.solutionService.updateEvaluation(aid, sid, this.evaluation._id, this.dto) - : this.solutionService.createEvaluation(aid, sid, this.dto); + ? this.evaluationService.update(aid, sid, this.evaluation._id, this.dto) + : this.evaluationService.create(aid, sid, this.dto); op.subscribe(result => { const op = this.evaluation ? 'updated' : 'created'; this.toastService.success('Evaluation', `Successfully ${op} evaluation${this.codeSearchInfo(result.codeSearch)}`); @@ -221,7 +225,7 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { } const {aid, sid} = this.route.snapshot.params; - this.solutionService.deleteEvaluation(aid, sid, this.evaluation._id).subscribe(result => { + this.evaluationService.delete(aid, sid, this.evaluation._id).subscribe(result => { this.toastService.warn('Evaluation', `Successfully deleted evaluation${this.codeSearchInfo(result.codeSearch)}`); }, error => { this.toastService.error('Evaluation', 'Failed to delete evaluation', error); diff --git a/frontend/src/app/assignment/modules/solution/similar-modal/similar-modal.component.ts b/frontend/src/app/assignment/modules/solution/similar-modal/similar-modal.component.ts index 8b60f34e9..e57f70871 100644 --- a/frontend/src/app/assignment/modules/solution/similar-modal/similar-modal.component.ts +++ b/frontend/src/app/assignment/modules/solution/similar-modal/similar-modal.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core'; import Task from "../../../model/task"; import Solution from "../../../model/solution"; import {CreateEvaluationDto, Evaluation, Snippet} from "../../../model/evaluation"; -import {ActivatedRoute, Router} from "@angular/router"; +import {ActivatedRoute} from "@angular/router"; import {AssignmentService} from "../../../services/assignment.service"; import {SolutionService} from "../../../services/solution.service"; import {filter, map, switchMap, tap} from "rxjs/operators"; @@ -10,6 +10,8 @@ import {TaskService} from "../../../services/task.service"; import {ConfigService} from "../../../services/config.service"; import {forkJoin} from "rxjs"; import {ToastService} from "@mean-stream/ngbx"; +import {EvaluationService} from "../../../services/evaluation.service"; +import {EmbeddingService} from "../../../services/embedding.service"; @Component({ selector: 'app-similar-modal', @@ -39,7 +41,9 @@ export class SimilarModalComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, private solutionService: SolutionService, + private evaluationService: EvaluationService, private assignmentService: AssignmentService, + private embeddingService: EmbeddingService, ) { } @@ -56,13 +60,13 @@ export class SimilarModalComponent implements OnInit { this.route.params.pipe( - switchMap(({aid, task}) => this.solutionService.getEvaluationValues(aid, 'solution', {task})), + switchMap(({aid, task}) => this.evaluationService.distinctValues(aid, 'solution', {task})), ).subscribe(ids => { this.existingEvaluations = Object.fromEntries(ids.map(id => [id, true])); }) this.route.params.pipe( - switchMap(({aid, sid, task}) => this.solutionService.getEvaluationByTask(aid, sid, task)), + switchMap(({aid, sid, task}) => this.evaluationService.findByTask(aid, sid, task)), filter((e): e is Evaluation => !!e), tap(evaluation => { this.evaluation = evaluation; @@ -70,7 +74,7 @@ export class SimilarModalComponent implements OnInit { this.dto.points = evaluation.points; }), switchMap(evaluation => forkJoin(evaluation.snippets - .map(snippet => this.solutionService.getSimilarEmbeddingSnippets(evaluation.assignment, evaluation.solution, snippet)) + .map(snippet => this.embeddingService.findSimilarSnippets(evaluation.assignment, evaluation.solution, snippet)) )), map(result => { this.snippets = {}; @@ -92,7 +96,7 @@ export class SimilarModalComponent implements OnInit { const assignment = this.route.snapshot.params.aid; forkJoin(Object.entries(this.selection) .filter(([, selected]) => selected) - .map(([solution]) => this.solutionService.createEvaluation(assignment, solution, { + .map(([solution]) => this.evaluationService.create(assignment, solution, { ...this.dto, snippets: this.snippets[solution] || [], })) diff --git a/frontend/src/app/assignment/modules/solution/solution.module.ts b/frontend/src/app/assignment/modules/solution/solution.module.ts index b30be860c..b76b56644 100644 --- a/frontend/src/app/assignment/modules/solution/solution.module.ts +++ b/frontend/src/app/assignment/modules/solution/solution.module.ts @@ -18,6 +18,7 @@ import {SolutionRoutingModule} from './solution-routing.module'; import {SolutionComponent} from './solution/solution.component'; import {SolutionTasksComponent} from './tasks/tasks.component'; import { SimilarModalComponent } from './similar-modal/similar-modal.component'; +import {CommentService} from "./comment.service"; @NgModule({ @@ -46,6 +47,9 @@ import { SimilarModalComponent } from './similar-modal/similar-modal.component'; ModalModule, NgbPopoverModule, ], + providers: [ + CommentService, + ], }) export class SolutionModule { } diff --git a/frontend/src/app/assignment/modules/solution/tasks/tasks.component.ts b/frontend/src/app/assignment/modules/solution/tasks/tasks.component.ts index 073158cb6..fbd1cb6b1 100644 --- a/frontend/src/app/assignment/modules/solution/tasks/tasks.component.ts +++ b/frontend/src/app/assignment/modules/solution/tasks/tasks.component.ts @@ -9,6 +9,7 @@ import Solution from '../../../model/solution'; import {AssignmentService} from '../../../services/assignment.service'; import {SolutionService} from '../../../services/solution.service'; import {TaskService} from '../../../services/task.service'; +import {EvaluationService} from "../../../services/evaluation.service"; @Component({ selector: 'app-solution-tasks', @@ -26,6 +27,7 @@ export class SolutionTasksComponent implements OnInit, OnDestroy { constructor( private assignmentService: AssignmentService, + private evaluationService: EvaluationService, private solutionService: SolutionService, private taskService: TaskService, private router: Router, @@ -40,7 +42,7 @@ export class SolutionTasksComponent implements OnInit, OnDestroy { this.solutionService.get(assignmentId, solutionId).pipe(tap(solution => { this.solution = solution; })), - this.solutionService.getEvaluations(assignmentId, solutionId).pipe(tap(evaluations => { + this.evaluationService.findAll(assignmentId, solutionId).pipe(tap(evaluations => { this.evaluations = {}; for (const evaluation of evaluations) { this.evaluations[evaluation.task] = evaluation; @@ -59,7 +61,7 @@ export class SolutionTasksComponent implements OnInit, OnDestroy { }); this.subscription = this.route.params.pipe( - switchMap(({aid, sid}) => this.solutionService.streamEvaluations(aid, sid)), + switchMap(({aid, sid}) => this.evaluationService.stream(aid, sid)), ).subscribe(({event, evaluation}) => { if (!this.assignment || !this.evaluations) { return; diff --git a/frontend/src/app/assignment/services/assignee.service.ts b/frontend/src/app/assignment/services/assignee.service.ts new file mode 100644 index 000000000..18429d4e2 --- /dev/null +++ b/frontend/src/app/assignment/services/assignee.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from "@angular/core"; +import {Observable} from "rxjs"; +import {Assignee} from "../model/assignee"; +import {environment} from "../../../environments/environment"; +import {HttpClient} from "@angular/common/http"; + +@Injectable() +export class AssigneeService { + constructor( + private http: HttpClient, + ) { + } + + getAssignees(assignment: string): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/assignees`); + } + + getAssignee(assignment: string, solution: string): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/assignee`); + } + + setAssignee(assignment: string, solution: string, assignee: string | undefined): Observable { + const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/assignee`; + return assignee ? this.http.put(url, {assignee}) : this.http.delete(url); + } +} diff --git a/frontend/src/app/assignment/services/assignment.service.ts b/frontend/src/app/assignment/services/assignment.service.ts index 20b0fb82c..670942eb0 100644 --- a/frontend/src/app/assignment/services/assignment.service.ts +++ b/frontend/src/app/assignment/services/assignment.service.ts @@ -15,9 +15,7 @@ import {CheckAssignment, CheckResult} from '../model/check'; import Course from '../model/course'; import {SearchResult, SearchSummary} from '../model/search-result'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class AssignmentService { constructor( private http: HttpClient, @@ -156,16 +154,13 @@ export class AssignmentService { } update(id: string, dto: UpdateAssignmentDto): Observable { - const token = this.getToken(id); - const headers = this.getHeaders(token); - return this.http.patch(`${environment.assignmentsApiUrl}/assignments/${id}`, dto, {headers}).pipe( + return this.http.patch(`${environment.assignmentsApiUrl}/assignments/${id}`, dto).pipe( tap(({token}) => token && this.setToken(id, token)), ); } delete(assignment: string): Observable { - const headers = this.getHeaders(this.getToken(assignment)); - return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}`, {headers}).pipe( + return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}`).pipe( tap(() => { this.setToken(assignment, null); this.deleteDraft(assignment); @@ -174,8 +169,7 @@ export class AssignmentService { } get(id: string): Observable { - const headers = this.getHeaders(this.getToken(id)); - return this.http.get(`${environment.assignmentsApiUrl}/assignments/${id}`, {headers}).pipe( + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${id}`).pipe( tap(a => { if ('token' in a && a.token) { this.setToken(id, a.token); @@ -185,41 +179,29 @@ export class AssignmentService { } search(id: string, q: string, context = 2, glob?: string, wildcard?: string): Observable { - const headers = this.getHeaders(this.getToken(id)); const params: Record = {q, context}; glob && (params.glob = glob); wildcard && (params.wildcard = wildcard); return this.http.get(`${environment.assignmentsApiUrl}/assignments/${id}/search`, { params, - headers, }); } searchSummary(id: string, q: string, glob?: string, wildcard?: string): Observable { - const headers = this.getHeaders(this.getToken(id)); const params: Record = {q}; glob && (params.glob = glob); wildcard && (params.wildcard = wildcard); return this.http.get(`${environment.assignmentsApiUrl}/assignments/${id}/search/summary`, { params, - headers, }); } moss(assignment: string): Observable { - const headers = this.getHeaders(this.getToken(assignment)); return this.http.put(`${environment.assignmentsApiUrl}/assignments/${assignment}/moss`, {}, { - headers, responseType: 'text', }); } - private getHeaders(token?: string | null | undefined): Record { - return token ? { - 'Assignment-Token': token, - } : {}; - } - getNext(course: Course, assignment: ReadAssignmentDto): Observable { const ids = course.assignments!; const index = ids.indexOf(assignment._id); diff --git a/frontend/src/app/assignment/services/course.service.ts b/frontend/src/app/assignment/services/course.service.ts index 8d26d3395..0d1d15084 100644 --- a/frontend/src/app/assignment/services/course.service.ts +++ b/frontend/src/app/assignment/services/course.service.ts @@ -9,9 +9,7 @@ import {environment} from '../../../environments/environment'; import {UserService} from '../../user/user.service'; import Course, {CourseStudent, CreateCourseDto, UpdateCourseDto} from '../model/course'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class CourseService { private _draft?: Course | CreateCourseDto | null; diff --git a/frontend/src/app/assignment/services/embedding.service.ts b/frontend/src/app/assignment/services/embedding.service.ts new file mode 100644 index 000000000..39601e18e --- /dev/null +++ b/frontend/src/app/assignment/services/embedding.service.ts @@ -0,0 +1,55 @@ +import {Injectable} from "@angular/core"; +import {Observable} from "rxjs"; +import {EstimatedCosts} from "../model/solution"; +import {environment} from "../../../environments/environment"; +import {HttpClient} from "@angular/common/http"; +import {Snippet} from "../model/evaluation"; +import {map} from "rxjs/operators"; + +@Injectable() +export class EmbeddingService { + constructor( + private http: HttpClient, + ) { + } + + import(assignment: string, estimate?: boolean): Observable { + return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`, {}, { + params: estimate ? {estimate} : undefined, + }); + } + + findTaskRelatedSnippets(assignment: string, solution: string, task: string): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`, { + params: { + solution, + task + } + }).pipe( + map(embeddings => embeddings.map(emb => this.convertEmbeddable(emb))), + ); + } + + findSimilarSnippets(assignment: string, solution: string, snippet: Snippet): Observable<(Snippet & { + solution: string + })[]> { + const id = `${solution}-${snippet.file}-${snippet.from.line}`; + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`, {params: {id}}).pipe( + map(embeddings => embeddings.map(emb => ({ + ...this.convertEmbeddable(emb), + solution: emb.solution, + }))), + ); + } + + private convertEmbeddable({file, line, text, _score}): Snippet { + return { + file, + from: {line, character: 0}, + to: {line: line + text.split('\n').length - 2, character: 0}, + comment: '', + score: _score, + code: text.substring(text.indexOf('\n') + 2), + }; + } +} diff --git a/frontend/src/app/assignment/services/evaluation.service.ts b/frontend/src/app/assignment/services/evaluation.service.ts new file mode 100644 index 000000000..ae339496b --- /dev/null +++ b/frontend/src/app/assignment/services/evaluation.service.ts @@ -0,0 +1,61 @@ +import {Injectable} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import { + CreateEvaluationDto, + Evaluation, + FilterEvaluationParams, + RemarkDto, + UpdateEvaluationDto +} from "../model/evaluation"; +import {Observable} from "rxjs"; +import {environment} from "../../../environments/environment"; +import {observeSSE} from "./sse-helper"; +import {map} from "rxjs/operators"; +import {TokenService} from "./token.service"; + +@Injectable() +export class EvaluationService { + constructor( + private http: HttpClient, + private tokenService: TokenService, + ) { + } + + findAll(assignment: string, solution?: string, params: FilterEvaluationParams = {}): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/${solution ? `solutions/${solution}/` : ''}evaluations`, {params: params as any}); + } + + distinctValues(assignment: string, field: keyof Evaluation | string, params: FilterEvaluationParams = {}): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/evaluations/unique/${field}`, {params: params as any}); + } + + distinctRemarks(assignment: string, params: FilterEvaluationParams = {}): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/evaluations/remarks`, {params: params as any}); + } + + stream(assignment: string, solution: string): Observable<{ event: string, evaluation: Evaluation }> { + const token = this.tokenService.getAssignmentToken(assignment); + return observeSSE(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/events?token=${token}`); + } + + findByTask(assignent: string, solution: string, task: string): Observable { + return this.findAll(assignent, solution, {task}).pipe(map(([first]) => first)); + } + + findOne(assignment: string, solution: string | undefined, evaluation: string): Observable { + // NB: The findOne endpoint does not really care about the solution, so we can just use * if unknown. + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution || '*'}/evaluations/${evaluation}`); + } + + create(assignment: string, solution: string, dto: CreateEvaluationDto): Observable { + return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations`, dto); + } + + update(assignment: string, solution: string, id: string, dto: UpdateEvaluationDto): Observable { + return this.http.patch(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/${id}`, dto); + } + + delete(assignment: string, solution: string, id: string): Observable { + return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/${id}`); + } +} diff --git a/frontend/src/app/assignment/services/selection.service.ts b/frontend/src/app/assignment/services/selection.service.ts index 6060ad447..93abfad72 100644 --- a/frontend/src/app/assignment/services/selection.service.ts +++ b/frontend/src/app/assignment/services/selection.service.ts @@ -12,9 +12,7 @@ interface SelectionDto { snippet: Snippet; } -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class SelectionService { constructor( private assignmentService: AssignmentService, diff --git a/frontend/src/app/assignment/services/solution-container.service.ts b/frontend/src/app/assignment/services/solution-container.service.ts index 2a14b59f9..5ebf4b8cd 100644 --- a/frontend/src/app/assignment/services/solution-container.service.ts +++ b/frontend/src/app/assignment/services/solution-container.service.ts @@ -8,9 +8,7 @@ import {UserService} from '../../user/user.service'; import Assignment from '../model/assignment'; import Solution from '../model/solution'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class SolutionContainerService { constructor( private http: HttpClient, diff --git a/frontend/src/app/assignment/services/solution.service.ts b/frontend/src/app/assignment/services/solution.service.ts index f302e547f..00df5b9c1 100644 --- a/frontend/src/app/assignment/services/solution.service.ts +++ b/frontend/src/app/assignment/services/solution.service.ts @@ -2,34 +2,17 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Params} from '@angular/router'; import {forkJoin, Observable, of} from 'rxjs'; -import {map, switchMap, tap} from 'rxjs/operators'; +import {switchMap, tap} from 'rxjs/operators'; import {environment} from '../../../environments/environment'; import {StorageService} from '../../services/storage.service'; import {UserService} from '../../user/user.service'; -import {Assignee} from '../model/assignee'; -import Assignment, {ReadAssignmentDto} from '../model/assignment'; +import {ReadAssignmentDto} from '../model/assignment'; import {CheckResult, CheckSolution} from '../model/check'; -import Comment from '../model/comment'; -import { - CreateEvaluationDto, - Evaluation, - FilterEvaluationParams, - RemarkDto, - Snippet, - UpdateEvaluationDto, -} from '../model/evaluation'; -import Solution, {AuthorInfo, EstimatedCosts, ImportSolution} from '../model/solution'; +import Solution, {AuthorInfo, ImportSolution} from '../model/solution'; import {AssignmentService} from './assignment.service'; -import {observeSSE} from './sse-helper'; -function asID(id: { _id?: string, id?: string } | string): string { - return typeof id === 'string' ? id : id._id! || id.id!; -} - -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class SolutionService { constructor( private http: HttpClient, @@ -58,33 +41,17 @@ export class SolutionService { this.storageService.set(`solutionDraft/${assignment}`, solution); } - // --------------- Comment Drafts --------------- - - getCommentDraft(solution: Solution | string): string | null { - const solutionID = asID(solution); - return this.storageService.get(`commentDraft/${solutionID}`); - } - - setCommentDraft(solution: Solution | string, draft: string | null) { - const solutionID = asID(solution); - this.storageService.set(`commentDraft/${solutionID}`, draft); - } - // --------------- Tokens --------------- - getToken(assignment: Assignment | string, id: string): string | null { - const assignmentID = asID(assignment); - return this.storageService.get(`solutionToken/${assignmentID}/${id}`); + getToken(assignment: string, id: string): string | null { + return this.storageService.get(`solutionToken/${assignment}/${id}`); } - setToken(assignment: Assignment | string, id: string, token: string | null): void { - const assignmentID = asID(assignment); - this.storageService.set(`solutionToken/${assignmentID}/${id}`, token); + setToken(assignment: string, id: string, token: string | null): void { + this.storageService.set(`solutionToken/${assignment}/${id}`, token); } - getOwnIds(assignment?: Assignment | string): { assignment: string, id: string }[] { - const assignmentID = assignment ? asID(assignment) : null; - + getOwnIds(assignment?: string): { assignment: string, id: string }[] { const pattern = /^solutionToken\/(.*)\/(.*)$/; const ids: { assignment: string, id: string; }[] = []; for (let i = 0; i < localStorage.length; i++) { @@ -95,7 +62,7 @@ export class SolutionService { } const matchedAssignmentID = match[1] as string; - if (assignmentID && matchedAssignmentID !== assignmentID) { + if (assignment && matchedAssignmentID !== assignment) { continue; } @@ -141,14 +108,10 @@ export class SolutionService { } previewImport(assignment: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/import`, {headers}); + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/import`); } import(assignment: string, files?: File[], usernames?: string[]): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); let body; if (files && files.length) { const data = new FormData(); @@ -158,35 +121,18 @@ export class SolutionService { body = data; } return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/import`, body, { - headers, params: usernames ? {usernames} : undefined, }); } - importEmbeddings(assignment: string, estimate?: boolean): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`, {}, { - headers, - params: estimate ? {estimate} : undefined, - }); - } - - get(assignment: Assignment | string, id: string): Observable { - const assignmentID = asID(assignment); - const headers = {}; - this.addSolutionToken(headers, assignmentID, id); - this.addAssignmentToken(headers, assignmentID); - return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignmentID}/solutions/${id}`, {headers}); + get(assignment: string, id: string): Observable { + return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${id}`); } getAll(assignment: string, search?: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); const params: Params = {}; search && (params.q = search); return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions`, { - headers, params, }); } @@ -196,192 +142,18 @@ export class SolutionService { } update(assignment: string, solution: string, dto: Partial): Observable { - const headers = {}; - this.addSolutionToken(headers, assignment, solution); - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}`; - return this.http.patch(url, dto, {headers}); + return this.http.patch(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}`, dto); } updateMany(assignment: string, dtos: Partial[]): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions`; - return this.http.patch(url, dtos, {headers}); + return this.http.patch(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions`, dtos); } delete(assignment: string, solution: string): Observable { - const headers = {}; - this.addSolutionToken(headers, assignment, solution); - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}`; - return this.http.delete(url, {headers}); + return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}`); } deleteAll(assignment: string, solutions: string[]): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions`; - return this.http.delete(url, {headers, params: {ids: solutions}}); - } - - getComments(assignment: string, solution: string): Observable { - const headers = {}; - this.addSolutionToken(headers, assignment, solution); - this.addAssignmentToken(headers, assignment); - - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments`; - return this.http.get(url, {headers}); - } - - postComment(assignment: string, solution: string, comment: Comment): Observable { - const headers = {}; - this.addSolutionToken(headers, assignment, solution); - this.addAssignmentToken(headers, assignment); - - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments`; - return this.http.post(url, comment, {headers}); - } - - deleteComment(assignment: string, solution: string, comment: string): Observable { - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments/${comment}`; - return this.http.delete(url); - } - - streamComments(assignment: string, solution: string): Observable<{ event: string, comment: Comment }> { - const token = this.getToken(assignment, solution) || this.assignmentService.getToken(assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/comments/events?token=${token}`; - return observeSSE(url); - } - - getEvaluations(assignment: Assignment | string, id?: string, params: FilterEvaluationParams = {}): Observable { - const assignmentID = asID(assignment); - const headers = {}; - if (id) { - this.addSolutionToken(headers, assignmentID, id); - } - this.addAssignmentToken(headers, assignmentID); - const url = `${environment.assignmentsApiUrl}/assignments/${assignmentID}/${id ? `solutions/${id}/` : ''}evaluations`; - return this.http.get(url, {headers, params: params as any}); - } - - getEvaluationValues(assignment: string, field: keyof Evaluation | string, params: FilterEvaluationParams = {}): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/evaluations/unique/${field}`; - return this.http.get(url, {headers, params: params as any}); - } - - getEvaluationRemarks(assignment: string, params: FilterEvaluationParams = {}): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/evaluations/remarks`; - return this.http.get(url, {headers, params: params as any}); - } - - streamEvaluations(assignment: string, solution: string): Observable<{ event: string, evaluation: Evaluation }> { - const token = this.assignmentService.getToken(assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/events?token=${token}`; - return observeSSE(url); - } - - getEvaluationByTask(assignent: string, solution: string, task: string): Observable { - return this.getEvaluations(assignent, solution, {task}).pipe(map(([first]) => first)); - } - - getEvaluation(assignment: string, solution: string | undefined, evaluation: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - if (solution) { - this.addSolutionToken(headers, assignment, solution); - } - // NB: The findOne endpoint does not really care about the solution, so we can just use * if unknown. - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/${solution || '*'}/evaluations/${evaluation}`; - return this.http.get(url, {headers}); - } - - createEvaluation(assignment: string, solution: string, dto: CreateEvaluationDto): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations`; - return this.http.post(url, dto, {headers}); - } - - updateEvaluation(assignment: string, solution: string, id: string, dto: UpdateEvaluationDto): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/${id}`; - return this.http.patch(url, dto, {headers}); - } - - deleteEvaluation(assignment: string, solution: string, id: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/evaluations/${id}`; - return this.http.delete(url, {headers}); - } - - getAssignees(assignment: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - return this.http.get(`${environment.assignmentsApiUrl}/assignments/${assignment}/assignees`, {headers}); - } - - setAssignee(assignment: string, solution: string, assignee: string | undefined): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/assignee`; - return assignee ? this.http.put(url, {assignee}, {headers}) : this.http.delete(url, {headers}); - } - - getEmbeddingSnippets(assignment: string, solution: string, task: string): Observable { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`; - return this.http.get(url, {headers, params: {solution, task}}).pipe( - map(embeddings => embeddings.map(emb => this.convertEmbeddable(emb))), - ); - } - - private convertEmbeddable({file, line, text, _score}): Snippet { - return { - file, - from: {line, character: 0}, - to: {line: line + text.split('\n').length - 2, character: 0}, - comment: '', - score: _score, - code: text.substring(text.indexOf('\n') + 2), - }; - } - - getSimilarEmbeddingSnippets(assignment: string, solution: string, snippet: Snippet): Observable<(Snippet & {solution: string})[]> { - const headers = {}; - this.addAssignmentToken(headers, assignment); - const url = `${environment.assignmentsApiUrl}/assignments/${assignment}/embeddings`; - const id = `${solution}-${snippet.file}-${snippet.from.line}`; - return this.http.get(url, {headers, params: {id}}).pipe( - map(embeddings => embeddings.map(emb => ({ - ...this.convertEmbeddable(emb), - solution: emb.solution, - }))), - ); - } - - private addAssignmentToken(headers: any, assignmentID: string) { - const assignmentToken = this.assignmentService.getToken(assignmentID); - if (assignmentToken) { - headers['Assignment-Token'] = assignmentToken; - } - } - - private addSolutionToken(headers: any, assignmentID: string, solutionID: string): string | null { - const token = this.getToken(assignmentID, solutionID); - if (token) { - headers['Solution-Token'] = token; - } - return token; + return this.http.delete(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions`, {params: {ids: solutions}}); } } diff --git a/frontend/src/app/assignment/services/task.service.ts b/frontend/src/app/assignment/services/task.service.ts index 194af29e9..909006de4 100644 --- a/frontend/src/app/assignment/services/task.service.ts +++ b/frontend/src/app/assignment/services/task.service.ts @@ -2,9 +2,7 @@ import {Injectable} from '@angular/core'; import {CreateEvaluationDto, Evaluation} from '../model/evaluation'; import Task from '../model/task'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class TaskService { find(tasks: Task[], id: string): Task | undefined { for (let task of tasks) { diff --git a/frontend/src/app/assignment/services/telemetry.service.ts b/frontend/src/app/assignment/services/telemetry.service.ts index 24244ef3b..5b98c95a1 100644 --- a/frontend/src/app/assignment/services/telemetry.service.ts +++ b/frontend/src/app/assignment/services/telemetry.service.ts @@ -3,21 +3,15 @@ import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {environment} from '../../../environments/environment'; import {CreateTelemetryDto, Telemetry} from '../model/telemetry'; -import {AssignmentService} from './assignment.service'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class TelemetryService { constructor( private http: HttpClient, - private assignmentService: AssignmentService, ) { } create(assignment: string, solution: string, dto: CreateTelemetryDto): Observable { - const token = this.assignmentService.getToken(assignment); - const headers: Record = token ? {'Assignment-Token': token} : {}; - return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/telemetry`, dto, {headers}); + return this.http.post(`${environment.assignmentsApiUrl}/assignments/${assignment}/solutions/${solution}/telemetry`, dto); } } diff --git a/frontend/src/app/assignment/services/token.interceptor.ts b/frontend/src/app/assignment/services/token.interceptor.ts new file mode 100644 index 000000000..bf643b85f --- /dev/null +++ b/frontend/src/app/assignment/services/token.interceptor.ts @@ -0,0 +1,31 @@ +import {Injectable} from '@angular/core'; +import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {TokenService} from "./token.service"; + +@Injectable() +export class TokenInterceptor implements HttpInterceptor { + constructor( + private tokenService: TokenService, + ) { + } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + const match = request.url.match(/\/assignments\/([^\/]+)(?:\/solutions\/([^\/]+))?/); + if (match) { + const headers: Record = {}; + const assignmentToken = this.tokenService.getAssignmentToken(match[1]); + if (assignmentToken) { + headers['Assignment-Token'] = assignmentToken; + } + const solutionToken = match[2] && this.tokenService.getSolutionToken(match[1], match[2]); + if (solutionToken) { + headers['Solution-Token'] = solutionToken; + } + request = request.clone({ + setHeaders: headers, + }); + } + return next.handle(request); + } +} diff --git a/frontend/src/app/assignment/services/token.service.ts b/frontend/src/app/assignment/services/token.service.ts new file mode 100644 index 000000000..25725f1e7 --- /dev/null +++ b/frontend/src/app/assignment/services/token.service.ts @@ -0,0 +1,18 @@ +import {Injectable} from "@angular/core"; +import {StorageService} from "../../services/storage.service"; + +@Injectable() +export class TokenService { + constructor( + private storage: StorageService, + ) { + } + + getAssignmentToken(assignment: string): string | null { + return this.storage.get(`assignmentToken/${assignment}`); + } + + getSolutionToken(assignment: string, solution: string): string | null { + return this.storage.get(`solutionToken/${assignment}/${solution}`); + } +} diff --git a/services/apps/assignments/src/solution/solution-auth.guard.ts b/services/apps/assignments/src/solution/solution-auth.guard.ts index bcfabefbd..e471fb89b 100644 --- a/services/apps/assignments/src/solution/solution-auth.guard.ts +++ b/services/apps/assignments/src/solution/solution-auth.guard.ts @@ -31,13 +31,20 @@ export class SolutionAuthGuard implements CanActivate { notFound(assignmentId); } + const privileged = this.assignmentService.isAuthorized(assignment, user, assignmentToken); + if (privileged) { + return true; + } + + if (solutionId === '*') { + return false; + } + const solution = await this.solutionService.findOne(solutionId); if (!solution) { notFound(solutionId); } - const privileged = this.assignmentService.isAuthorized(assignment, user, assignmentToken); - const authorized = this.solutionService.isAuthorized(solution, user, solutionToken); - return privileged || authorized; + return this.solutionService.isAuthorized(solution, user, solutionToken); } }