Skip to content

Commit

Permalink
Merge pull request #365 from fujaba/feat/assignment-members
Browse files Browse the repository at this point in the history
Assignment Members
  • Loading branch information
Clashsoft authored Oct 18, 2023
2 parents 3c57e2b + 0efcb38 commit a9baeb4
Show file tree
Hide file tree
Showing 47 changed files with 1,243 additions and 882 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {SubmitModalComponent} from './submit-modal/submit-modal.component';
import {AssignmentTasksComponent} from './tasks/tasks.component';
import {StatisticsService} from "./statistics.service";
import {SubmitService} from "./submit.service";
import {MemberService} from "./member.service";
import {UserModule} from "../../../user/user.module";

@NgModule({
declarations: [
Expand Down Expand Up @@ -54,8 +56,10 @@ import {SubmitService} from "./submit.service";
NgbAccordionModule,
RouteTabsModule,
ModalModule,
UserModule,
],
providers: [
MemberService,
StatisticsService,
SubmitService,
],
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/app/assignment/modules/assignment/member.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {Member} from "../../../user/member";
import {environment} from "../../../../environments/environment";

@Injectable()
export class MemberService {
constructor(
private http: HttpClient,
) {
}

findAll(id: string): Observable<Member[]> {
return this.http.get<Member[]>(`${environment.assignmentsApiUrl}/assignments/${id}/members`);
}

update(member: Member): Observable<Member> {
const {_user, parent, user, ...rest} = member;
return this.http.put<Member>(`${environment.assignmentsApiUrl}/assignments/${parent}/members/${user}`, rest).pipe(
tap(newMember => newMember._user = _user),
);
}

delete({parent, user}: Member): Observable<Member> {
return this.http.delete<Member>(`${environment.assignmentsApiUrl}/assignments/${parent}/members/${user}`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@
<h5>
Student Link
</h5>
<p>
You can now give your students the following link:
</p>
<div class="input-group">
<input #linkInput type="text" class="form-control" readonly [value]="origin + '/assignments/' + id">
<input #linkInput type="text" class="form-control" readonly [value]="origin + '/assignments/' + assignment?._id">
<button *ngxClipboardIfSupported type="button" class="btn btn-outline-secondary bi-clipboard" ngbTooltip="Copy" [ngxClipboard]="linkInput">
</button>
</div>
<div class="form-text">
Share this link with your students. They will be able to submit solutions to this assignment.
</div>
<hr/>
<h5 class="d-flex align-items-center">
Members
<app-user-typeahead class="ms-auto" [(user)]="newMember"></app-user-typeahead>
<button class="btn btn-success bi-person-add ms-2" [disabled]="!newMember" (click)="addMember()">
Add Member
</button>
</h5>
<div class="mb-3">
<app-member-list [members]="members" [owner]="assignment?.createdBy" (deleted)="deleteMember($event)"></app-member-list>
<div class="form-text">
Add teaching assistants and other people who should have full access to this assignment.
</div>
</div>
<hr/>
<div class="alert alert-warning">
The following fields contain the assignment token, a secret key that can be used to access and grade all submissions.
Expand All @@ -19,29 +33,21 @@ <h5>
If you suspect this token may have leaked, you can
<a routerLink="." (click)="regenerateToken()">generate a new token</a>.
</div>
<h5>
Access Token
</h5>
<p>
To view submissions on other devices, you may be asked for the following token.
</p>
<app-token-input [value]="token || ''"></app-token-input>
<hr/>
<h5>
Teaching Assistant Invitation
</h5>
<p>
You can invite Teaching Assistants by giving them the following link.
</p>
<app-token-input [value]="origin + '/assignments/' + id + '/solutions?atok=' + token"></app-token-input>
<hr/>
<h5>
fulibFeedback Settings
</h5>
<p>
Use the button below to configure fulibFeedback for this assignment.
</p>
<a class="btn btn-primary bi-magic" [href]="ide + '://fulib.fulibfeedback/configure?api_server=' + encodedApiServer + '&assignment=' + id + '&token=' + token | safeUrl">
Configure
</a>
<ng-container *ngIf="assignment && assignment['token']">
<h5>
Access Token
</h5>
<app-token-input [value]="assignment['token'] || ''"></app-token-input>
<div class="form-text">
To view submissions on other devices, you may be asked for this token.
</div>
<hr/>
<h5>
Teaching Assistant Invitation
</h5>
<app-token-input [value]="origin + '/assignments/' + assignment._id + '/solutions?atok=' + assignment['token']"></app-token-input>
<div class="form-text">
You can invite Teaching Assistants by giving them this link.
</div>
</ng-container>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,77 @@ import {DOCUMENT} from '@angular/common';
import {Component, Inject, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {switchMap, tap} from 'rxjs/operators';
import {environment} from '../../../../../environments/environment';
import {AssignmentService} from '../../../services/assignment.service';
import {ConfigService} from '../../../services/config.service';
import Assignment, {ReadAssignmentDto} from "../../../model/assignment";
import {MemberService} from "../member.service";
import {Member} from "../../../../user/member";
import {User} from "../../../../user/user";
import {forkJoin} from "rxjs";
import {UserService} from "../../../../user/user.service";

@Component({
selector: 'app-assignment-share',
templateUrl: './share.component.html',
styleUrls: ['./share.component.scss'],
})
export class ShareComponent implements OnInit {
id: string;
token?: string;
assignment?: Assignment | ReadAssignmentDto;
members: Member[];

ide = this.configService.get('ide');
newMember?: User;

readonly origin: string;
readonly encodedApiServer = encodeURIComponent(new URL(environment.assignmentsApiUrl, location.origin).origin);

constructor(
private assignmentService: AssignmentService,
private memberService: MemberService,
private userService: UserService,
private route: ActivatedRoute,
private configService: ConfigService,
@Inject(DOCUMENT) document: Document,
) {
this.origin = document.location.origin;
}

ngOnInit(): void {
this.route.params.pipe(
tap(({aid}) => this.id = aid),
switchMap(({aid}) => this.assignmentService.get(aid)),
).subscribe(assignment => {
if ('token' in assignment) {
this.token = assignment.token;
}
this.assignment = assignment;
});

this.route.params.pipe(
switchMap(({aid}) => this.memberService.findAll(aid)),
tap(members => this.members = members),
switchMap(members => forkJoin(members.map(member => this.userService.findOne(member.user).pipe(
tap(user => member._user = user),
)))),
).subscribe();
}

regenerateToken() {
if (!confirm('Are you sure you want to generate a new token? You will need to re-send the invitation to teaching assistants.')) {
return;
}

this.assignmentService.update(this.id, {token: true}).subscribe(assignment => {
this.token = assignment.token!;
this.assignmentService.update(this.assignment!._id, {token: true}).subscribe(assignment => {
this.assignment = assignment;
});
}

addMember() {
this.memberService.update({
parent: this.assignment!._id,
user: this.newMember!.id!,
_user: this.newMember!,
}).subscribe(member => {
this.members.push(member);
this.newMember = undefined;
});
}

deleteMember(member: Member) {
this.memberService.delete(member).subscribe(() => {
this.members.splice(this.members.indexOf(member), 1);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<a ngbDropdownItem [routerLink]="['/assignments', assignment._id, 'import']" class="bi-download">
Import Solutions
</a>
<a ngbDropdownItem class="bi-magic"
ngbTooltip="Automatically configure fulibFeedback for this assigmnent in your IDE"
[href]="ide + '://fulib.fulibfeedback/configure?api_server=' + encodedApiServer + '&assignment=' + assignment._id + '&token=' + assignment['token'] | safeUrl">
Configure fulibFeedback
</a>
<div class="dropdown-divider"></div>
<button ngbDropdownItem class="text-warning bi-archive" (click)="archive()">
{{ assignment.archived ? 'Unarchive' : 'Archive' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Component, EventEmitter, Input, Output} from '@angular/core';
import {ToastService} from '@mean-stream/ngbx';
import {ReadAssignmentDto} from '../../../model/assignment';
import {AssignmentService} from '../../../services/assignment.service';
import {ConfigService} from "../../../services/config.service";
import {environment} from "../../../../../environments/environment";

@Component({
selector: 'app-assignment-actions',
Expand All @@ -12,10 +14,15 @@ export class AssignmentActionsComponent {
@Input() assignment: ReadAssignmentDto;
@Output() removed = new EventEmitter<void>();

readonly ide: string;
readonly encodedApiServer = encodeURIComponent(new URL(environment.assignmentsApiUrl, globalThis.location?.origin).origin);

constructor(
private assignmentService: AssignmentService,
private toastService: ToastService,
configService: ConfigService,
) {
this.ide = configService.get('ide');
}

archive() {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/assignment/services/assignment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class AssignmentService {
return this.http.get<ReadAssignmentDto[]>(`${environment.assignmentsApiUrl}/assignments`, {
params: {
...(ids ? {ids: ids.join(',')} : {}),
...(createdBy ? {createdBy} : {}),
...(createdBy ? {createdBy, members: [createdBy]} : {}),
...(archived !== undefined ? {archived} : {}),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export class EditMemberComponent implements OnInit {
}

this.memberService.update({
projectId: this.route.snapshot.params.id,
userId: this.user.id!,
user: this.user,
parent: this.route.snapshot.params.id,
user: this.user.id!,
_user: this.user,
}).subscribe(() => {
this.user = undefined;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class ProjectListComponent implements OnInit, OnDestroy {
return;
}

this.memberService.delete({projectId: project.id, userId: this.currentUser!.id!}).subscribe(() => {
this.memberService.delete({parent: project.id, user: this.currentUser!.id!}).subscribe(() => {
this.projects.removeFirst(p => p === project);
this.toastService.warn('Leave Project', 'Successfully left project.');
}, error => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,7 @@ <h4 class="d-flex align-items-center">
Add
</a>
</h4>
<ul class="list-group">
<li *ngFor="let member of members" class="list-group-item d-flex align-items-center">
<div class="me-auto">
{{ member.user
? member.user.firstName + ' ' + member.user.lastName + ' (' + member.user.username + ')'
: 'Loading...' }}
<span class="text-muted">
&bull;
{{ member.userId === project.userId ? 'Owner' : 'Collaborator' }}
</span>
</div>
<button *ngIf="member.userId !== project.userId"
type="button" class="btn btn-sm btn-danger"
ngbTooltip="Remove Collaborator"
(click)="delete(member)">
<i class="bi-person-x"></i>
</button>
</li>
</ul>
<app-member-list [members]="members" [owner]="project.userId" (deleted)="delete($event)"></app-member-list>
</ng-container>
<ng-container *ngIf="project && (currentUser && currentUser.id === project.userId)">
<hr/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {forkJoin} from 'rxjs';
import {map, switchMap, tap} from 'rxjs/operators';
import {User} from '../../../user/user';
import {UserService} from '../../../user/user.service';
import {Member} from '../../model/member';
import {Member} from '../../../user/member';
import {Project} from '../../model/project';
import {MemberService} from '../../services/member.service';
import {ProjectService} from '../../services/project.service';
Expand Down Expand Up @@ -45,8 +45,8 @@ export class SettingsComponent implements OnInit {
id$.pipe(
switchMap(id => this.memberService.findAll(id)),
tap(members => this.members = members),
switchMap(members => forkJoin(members.map(member => this.userService.findOne(member.userId).pipe(
tap(user => member.user = user),
switchMap(members => forkJoin(members.map(member => this.userService.findOne(member.user).pipe(
tap(user => member._user = user),
)))),
).subscribe();
}
Expand All @@ -59,7 +59,7 @@ export class SettingsComponent implements OnInit {
}

delete(member: Member) {
if (!confirm(`Are you sure you want to revoke Collaborator status from ${member.user?.firstName} ${member.user?.lastName}? They can be added as a collaborator again later.`)) {
if (!confirm(`Are you sure you want to revoke Collaborator status from ${member._user?.firstName} ${member._user?.lastName}? They can be added as a collaborator again later.`)) {
return;
}

Expand Down
8 changes: 0 additions & 8 deletions frontend/src/app/projects/model/member.ts

This file was deleted.

12 changes: 6 additions & 6 deletions frontend/src/app/projects/services/member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {environment} from '../../../environments/environment';
import {Member} from '../model/member';
import {Member} from "../../user/member";

@Injectable()
export class MemberService {
Expand All @@ -17,13 +17,13 @@ export class MemberService {
}

update(member: Member): Observable<Member> {
const {user, ...rest} = member;
return this.http.put<Member>(`${environment.projectsApiUrl}/projects/${member.projectId}/members/${member.userId}`, rest).pipe(
tap(newMember => newMember.user = user),
const {_user, parent, user, ...rest} = member;
return this.http.put<Member>(`${environment.projectsApiUrl}/projects/${parent}/members/${user}`, rest).pipe(
tap(newMember => newMember._user = _user),
);
}

delete({projectId, userId}: Member): Observable<Member> {
return this.http.delete<Member>(`${environment.projectsApiUrl}/projects/${projectId}/members/${userId}`);
delete({parent, user}: Member): Observable<Member> {
return this.http.delete<Member>(`${environment.projectsApiUrl}/projects/${parent}/members/${user}`);
}
}
16 changes: 16 additions & 0 deletions frontend/src/app/user/member-list/member-list.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<ul class="list-group">
<li *ngFor="let member of members; let index = index" class="list-group-item d-flex align-items-center">
<div class="me-auto">
{{ member._user
? member._user.firstName + ' ' + member._user.lastName + ' (' + member._user.username + ')'
: 'Loading...' }}
<span class="text-muted">&bull; {{ member.role || (member.user === owner ? 'Owner' : 'Collaborator') }}</span>
</div>
<button
*ngIf="member.user !== owner && deleted.observed"
type="button" class="btn btn-sm btn-danger bi-person-x"
ngbTooltip="Remove Collaborator"
(click)="delete(member)"
></button>
</li>
</ul>
Empty file.
Loading

0 comments on commit a9baeb4

Please sign in to comment.