Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assignments Fixes #431

Merged
merged 9 commits into from
Oct 8, 2024
2 changes: 0 additions & 2 deletions .github/workflows/test-frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ jobs:
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit
run: pnpm audit
- name: Lint
run: pnpm lint
- name: Test
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/test-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ jobs:
cache-dependency-path: services/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit
run: pnpm audit
- name: Lint
run: pnpm lint
- name: Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="p-3 d-flex align-items-center sticky-top bg-body-secondary" style="top: 3.5rem">
<div class="p-3 d-flex align-items-center sticky-top bg-body-secondary">
<div class="position-relative me-sm-2 flex-grow-1">
<input type="search" class="form-control" id="searchInput" [class.is-invalid]="searchError"
placeholder="Search for Name, Student ID, E-Mail, GitHub Username, Assignee"
Expand All @@ -18,23 +18,31 @@
popoverTitle="Search Help"
placement="bottom-right"
></button>
<div *ngFor="let criteria of [
@for (criteria of [
{id: 'assignee', title: 'Assignee', icon: 'bi-person-check', options: assigneeNames},
{id: 'status', title: 'Status', icon: 'bi-award', options: solutionStatus},
]" ngbDropdown #dropdown=ngbDropdown class="d-inline-block me-2">
<button class="btn btn-outline-secondary" [id]="criteria.id + '-dropdown'" ngbDropdownToggle [ngbTooltip]="criteria.title">
<i [class]="criteria.icon"></i>
<span class="visually-hidden">{{ criteria.title }}</span>
</button>
<div ngbDropdownMenu [attr.aria-labelledby]="criteria.id + '-dropdown'">
@if (dropdown.isOpen()) {
<button *ngFor="let option of criteria.options" ngbDropdownItem (click)="toggleSearch(criteria.id, option)"
class="dropdown-item-check" [class.checked]="hasSearch(criteria.id, option)">
{{ option }}
</button>
}
]; track criteria.id) {
<div ngbDropdown #dropdown=ngbDropdown class="d-inline-block me-2">
<button class="btn btn-outline-secondary" [id]="criteria.id + '-dropdown'" ngbDropdownToggle [ngbTooltip]="criteria.title">
<i [class]="criteria.icon"></i>
<span class="visually-hidden">{{ criteria.title }}</span>
</button>
<div ngbDropdownMenu [attr.aria-labelledby]="criteria.id + '-dropdown'">
@if (dropdown.isOpen()) {
@for (option of criteria.options; track option) {
<button ngbDropdownItem (click)="toggleSearch(criteria.id, option)"
class="dropdown-item-check" [class.checked]="hasSearch(criteria.id, option)">
{{ option }}
</button>
} @empty {
<div class="dropdown-item-text text-muted">
No {{ criteria.title }} available
</div>
}
}
</div>
</div>
</div>
}
<div style="width: 12rem">
@if ((selected | keyvalue).length;as length) {
{{ length }} /
Expand All @@ -51,16 +59,18 @@
<span class="visually-hidden">Bulk Actions</span>
</button>
<div ngbDropdownMenu aria-labelledby="actionsDropdown">
<div class="dropdown-header">
Assign {{ count }} Solutions to...
</div>
@for (assignee of assigneeNames; track assignee) {
<button ngbDropdownItem (click)="assignAll(assignee)">
<span class="badge" [style.background-color]="assignee | assigneeColor">{{ assignee | initials }}</span>
{{ assignee }}
</button>
@if (assigneeNames.length) {
<div class="dropdown-header">
Assign {{ count }} Solutions to...
</div>
@for (assignee of assigneeNames; track assignee) {
<button ngbDropdownItem (click)="assignAll(assignee)">
<span class="badge" [style.background-color]="assignee | assigneeColor">{{ assignee | initials }}</span>
{{ assignee }}
</button>
}
<div class="dropdown-divider"></div>
}
<div class="dropdown-divider"></div>
<button ngbDropdownItem (click)="openSelected()">
Open {{ count }} Solutions in new tabs
</button>
Expand Down Expand Up @@ -228,14 +238,12 @@
ngbTooltip="View Solution"
></a>
@if (assignment && assignment.classroom?.org && assignment.classroom?.prefix && solution.author.github) {
<div class="btn-group me-1">
<a
class="btn btn-secondary bi-github"
[href]="assignment|githubLink:solution:true"
target="_blank"
ngbTooltip="View {{solution.commit ? 'Commit' : 'Repository'}} on GitHub"
></a>
</div>
<a
class="btn btn-secondary bi-github me-1"
[href]="assignment|githubLink:solution:true"
target="_blank"
ngbTooltip="View {{solution.commit ? 'Commit' : 'Repository'}} on GitHub"
></a>
}
<a
class="btn btn-primary me-1 bi-check-circle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
.table-centered th {
vertical-align: middle;
}

.sticky-top {
top: 3.5rem;
z-index: 990;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</label>
@for (model of embeddingModels; track model.id) {
<div class="form-check">
<input class="form-check-input" type="radio" name="openaiModel" id="openaiModel-{{model.id}}" [value]="model.id" [ngModel]="openAI.model">
<input class="form-check-input" type="radio" name="openaiModel" id="openaiModel-{{model.id}}" [value]="model.id" [(ngModel)]="openAI.model">
<label class="form-check-label" for="openaiModel-{{model.id}}">
{{ model.id }}
<span class="badge bg-{{ model.labelBg }}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import Solution, {
import {map} from "rxjs/operators";
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";
import {ImportTab} from '../import-tab.interface';

@Component({
selector: 'app-import-consent',
templateUrl: './import-consent.component.html',
styleUrls: ['./import-consent.component.scss']
})
export class ImportConsentComponent {
export class ImportConsentComponent implements ImportTab {
consentText = '';

constructor(
Expand All @@ -25,7 +26,7 @@ export class ImportConsentComponent {
) {
}

import(): Observable<ImportSolution[]> {
import() {
const assignment = this.route.snapshot.params.aid;
const lines = this.consentText.split('\n');
const splitter = /[\t,;]/;
Expand All @@ -51,6 +52,9 @@ export class ImportConsentComponent {
updates.push({author, consent});
}
}
return this.solutionService.updateMany(assignment, updates).pipe(map(results => results.filter(s => s)));
return this.solutionService.updateMany(assignment, updates).pipe(
map(results => results.filter(s => s)),
map(results => `Successfully updated ${results.length} solutions`),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {Component, OnInit} from '@angular/core';
import {EstimatedCosts} from "../../../model/solution";
import {ActivatedRoute} from "@angular/router";
import {switchMap, tap} from "rxjs/operators";
import {map, switchMap, tap} from 'rxjs/operators';
import {EmbeddingService} from "../../../services/embedding.service";
import {ImportTab} from '../import-tab.interface';

@Component({
selector: 'app-import-embeddings',
templateUrl: './import-embeddings.component.html',
styleUrls: ['./import-embeddings.component.scss']
})
export class ImportEmbeddingsComponent implements OnInit {
export class ImportEmbeddingsComponent implements OnInit, ImportTab {
costs?: EstimatedCosts;
costsAreFinal = false;

Expand All @@ -36,6 +37,7 @@ export class ImportEmbeddingsComponent implements OnInit {
this.costs = result;
this.costsAreFinal = true;
}),
map(result => `Successfully imported ${result.functions.length} function embeddings via OpenAI.`),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {Component} from '@angular/core';
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";
import {ImportTab} from '../import-tab.interface';
import {map} from 'rxjs/operators';

@Component({
selector: 'app-import-files',
templateUrl: './import-files.component.html',
styleUrls: ['./import-files.component.scss']
})
export class ImportFilesComponent {
export class ImportFilesComponent implements ImportTab {
files: File[] = [];

constructor(
Expand All @@ -22,6 +24,8 @@ export class ImportFilesComponent {

import() {
const assignmentId = this.route.snapshot.params.aid;
return this.solutionService.import(assignmentId, this.files);
return this.solutionService.import(assignmentId, this.files).pipe(
map(results => `Successfully imported ${results.length} solutions from files`),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,42 @@ import {Component} from '@angular/core';
import {ImportSolution} from "../../../model/solution";
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";
import {ToastService} from '@mean-stream/ngbx';
import {ImportTab} from '../import-tab.interface';
import {map} from 'rxjs/operators';

@Component({
selector: 'app-import-github',
templateUrl: './import-github.component.html',
styleUrls: ['./import-github.component.scss']
})
export class ImportGithubComponent {
export class ImportGithubComponent implements ImportTab {
checkedUsernames: Partial<Record<string, boolean>> = {};
previewSolutions: ImportSolution[];
previewSolutions?: ImportSolution[];
reimport = false;

constructor(
private solutionService: SolutionService,
private route: ActivatedRoute,
private toastService: ToastService,
) {
}

previewGitHubImport() {
this.solutionService.previewImport(this.route.snapshot.params.aid).subscribe(solutions => this.previewSolutions = solutions);
this.solutionService.previewImport(this.route.snapshot.params.aid).subscribe({
next: solutions => this.previewSolutions = solutions,
error: error => {
this.toastService.error('Load Students', 'Failed to load Students from GitHub', error);
this.previewSolutions = [];
},
});
}

import() {
const assignmentId = this.route.snapshot.params.aid;
const usernames = Object.keys(this.checkedUsernames).filter(username => this.checkedUsernames[username]);
return this.solutionService.import(assignmentId, undefined, usernames, this.reimport);
return this.solutionService.import(assignmentId, undefined, usernames, this.reimport).pipe(
map(results => `Successfully imported ${results.length} solutions from GitHub`),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,13 @@ export class ImportModalComponent {
import(component: any) {
this.importing = true;
component.import().subscribe({
next: results => {
next: message => {
this.importing = false;
if (typeof results === 'string') {
this.toastService.success('Import', 'Successfully ran MOSS');
} else if (results && typeof results === 'object' && 'length' in results) {
this.toastService.success('Import', `Successfully imported ${results.length} solutions`);
} else {
this.toastService.success('Import', 'Successfully imported embeddings');
}
this.toastService.success('Import', message);
},
error: error => {
this.importing = false;
this.toastService.error('Import', 'Failed to import solutions', error);
this.toastService.error('Import', `Failed to import: ${error.message}`, error);
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {Component} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {AssignmentService} from "../../../services/assignment.service";
import {tap} from "rxjs/operators";
import {map, tap} from 'rxjs/operators';
import {ImportTab} from '../import-tab.interface';

@Component({
selector: 'app-import-moss',
templateUrl: './import-moss.component.html',
styleUrls: ['./import-moss.component.scss']
})
export class ImportMossComponent {
export class ImportMossComponent implements ImportTab {
mossResult?: string;

constructor(
Expand All @@ -21,6 +22,7 @@ export class ImportMossComponent {
const assignmentId = this.route.snapshot.params.aid;
return this.assignmentService.moss(assignmentId).pipe(
tap(result => this.mossResult = result),
map(result => `Successfully imported to MOSS. Results are available at: ${result}`),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {Observable} from 'rxjs';

export interface ImportTab {
import(): Observable<string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,19 @@ export class AssignmentController {
@Param('id', ObjectIdPipe) id: Types.ObjectId,
@Body() dto: UpdateAssignmentDto,
): Promise<Assignment | null> {
const {token, classroom, ...rest} = dto;
const {token, ...rest} = dto;
const update: UpdateQuery<Assignment> = rest;
if (token) {
update.token = generateToken();
}
if (classroom) {
// need to flatten the classroom object to prevent deleting the GitHub token all the time
for (const [key, value] of Object.entries(classroom)) {
update[`classroom.${key}`] = value;
for (const key of ['classroom', 'openAI', 'moss'] as const) {
const obj = rest[key];
if (obj) {
delete update[key];
// need to flatten these objects to prevent deleting the tokens all the time
for (const [subKey, value] of Object.entries(obj)) {
update[`${key}.${subKey}`] = value;
}
}
}
return this.assignmentService.update(id, update);
Expand Down
22 changes: 9 additions & 13 deletions services/apps/assignments/src/embedding/embedding.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,10 @@ export class EmbeddingService implements OnModuleInit {
index: 'embeddings',
query: {
bool: {
must: {
term: {
assignment,
type: 'task',
},
},
filter: [
{term: {assignment}},
{term: {type: 'task'}},
],
must_not: {
terms: {
task: tasks,
Expand All @@ -260,7 +258,7 @@ export class EmbeddingService implements OnModuleInit {
index: 'embeddings',
query: {
bool: {
must: {
filter: {
term: {
assignment,
},
Expand All @@ -276,12 +274,10 @@ export class EmbeddingService implements OnModuleInit {
index: 'embeddings',
query: {
bool: {
must: {
term: {
assignment,
solution,
},
},
filter: [
{term: {assignment}},
{term: {solution}},
],
},
},
});
Expand Down
Loading