Skip to content

Commit

Permalink
Import students into exam (ls1intum#1667)
Browse files Browse the repository at this point in the history
  • Loading branch information
madwau authored Jun 18, 2020
1 parent aa5c1e9 commit f53e29a
Show file tree
Hide file tree
Showing 17 changed files with 1,027 additions and 23 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@ngx-translate/http-loader": "4.0.0",
"@sentry/browser": "5.17.0",
"@swimlane/ngx-datatable": "17.0.0",
"@types/papaparse": "^5.0.4",
"ace-builds": "1.4.11",
"angular-fittext": "2.1.1",
"blob-util": "2.0.2",
Expand Down Expand Up @@ -67,6 +68,7 @@
"ngx-moment": "4.0.1",
"ngx-treeview": "6.0.2",
"ngx-webstorage": "5.0.0",
"papaparse": "5.2.0",
"rxjs": "6.5.5",
"rxjs-compat": "6.5.5",
"showdown": "1.9.1",
Expand Down
29 changes: 19 additions & 10 deletions src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,25 @@ public ResponseEntity<Exam> updateExam(@PathVariable Long courseId, @RequestBody
/**
* GET /courses/{courseId}/exams/{examId} : Find an exam by id.
*
* @param courseId the course to which the exam belongs
* @param examId the exam to find
* @param courseId the course to which the exam belongs
* @param examId the exam to find
* @param withStudents boolean flag whether to include all students registered for the exam
* @return the ResponseEntity with status 200 (OK) and with the found exam as body
*/
@GetMapping("/courses/{courseId}/exams/{examId}")
@PreAuthorize("hasAnyRole('ADMIN', 'INSTRUCTOR')")
public ResponseEntity<Exam> getExam(@PathVariable Long courseId, @PathVariable Long examId) {
public ResponseEntity<Exam> getExam(@PathVariable Long courseId, @PathVariable Long examId, @RequestParam(defaultValue = "false") boolean withStudents) {
log.debug("REST request to get exam : {}", examId);
Optional<ResponseEntity<Exam>> courseAndExamAccessFailure = examAccessService.checkCourseAndExamAccess(courseId, examId);
return courseAndExamAccessFailure.orElseGet(() -> ResponseEntity.ok(examService.findOne(examId)));
if (courseAndExamAccessFailure.isPresent()) {
return courseAndExamAccessFailure.get();
}
if (!withStudents) {
return ResponseEntity.ok(examService.findOne(examId));
}
Exam exam = examService.findOneWithRegisteredUsers(examId);
exam.getRegisteredUsers().forEach(user -> user.setVisibleRegistrationNumber(user.getRegistrationNumber()));
return ResponseEntity.ok(exam);
}

/**
Expand Down Expand Up @@ -249,10 +258,10 @@ public ResponseEntity<Void> addStudentToExam(@PathVariable Long courseId, @PathV
* This method first tries to find the student in the internal Artemis user database (because the user is most probably already using Artemis).
* In case the user cannot be found, we additionally search the (TUM) LDAP in case it is configured properly.
*
* @param courseId the id of the course
* @param examId the id of the exam
* @param studentDtos the list of students (with at least registration number) who should get access to the exam
* @return the list of students who could not be registered for the exam, because they could NOT be found in the Artemis database and could NOT be found in the TUM LDAP
* @param courseId the id of the course
* @param examId the id of the exam
* @param studentDtos the list of students (with at least registration number) who should get access to the exam
* @return the list of students who could not be registered for the exam, because they could NOT be found in the Artemis database and could NOT be found in the TUM LDAP
*/
@PostMapping(value = "/courses/{courseId}/exams/{examId}/students")
@PreAuthorize("hasAnyRole('INSTRUCTOR', 'ADMIN')")
Expand All @@ -274,21 +283,21 @@ public ResponseEntity<List<StudentDTO>> addStudentsToExam(@PathVariable Long cou
Optional<User> optionalStudent = userService.findUserWithGroupsAndAuthoritiesByRegistrationNumber(registrationNumber);
if (optionalStudent.isPresent()) {
var student = optionalStudent.get();
exam.addUser(student);
// we only need to add the student to the course group, if the student is not yet part of it, otherwise the student cannot access the exam (within the course)
if (!student.getGroups().contains(course.getStudentGroupName())) {
userService.addUserToGroup(student, course.getStudentGroupName());
}
exam.addUser(student);
continue;
}
// 2) if we cannot find the student, we use the registration number and try to find the student in the (TUM) LDAP, create it in the Artemis DB and in a potential
// external user management system
optionalStudent = userService.createUserFromLdap(registrationNumber);
if (optionalStudent.isPresent()) {
var student = optionalStudent.get();
exam.addUser(student);
// the newly created student needs to get the rights to access the course, otherwise the student cannot access the exam (within the course)
userService.addUserToGroup(student, course.getStudentGroupName());
exam.addUser(student);
continue;
}
// 3) if we cannot find the user in the (TUM) LDAP, we report this to the client
Expand Down
5 changes: 5 additions & 0 deletions src/main/webapp/app/entities/student-dto.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class StudentDTO {
public firstName: string;
public lastName: string;
public registrationNumber: string;
}
4 changes: 4 additions & 0 deletions src/main/webapp/app/exam/manage/exam-management.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo
import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module';
import { MomentModule } from 'ngx-moment';
import { DurationPipe } from 'app/shared/pipes/artemis-duration.pipe';
import { StudentsExamImportDialogComponent } from 'app/exam/manage/students/students-exam-import-dialog/students-exam-import-dialog.component';
import { StudentsExamImportButtonComponent } from 'app/exam/manage/students/students-exam-import-dialog/students-exam-import-button.component';

const ENTITY_STATES = [...examManagementState];

Expand All @@ -46,6 +48,8 @@ const ENTITY_STATES = [...examManagementState];
ExerciseGroupDetailComponent,
ExamStudentsComponent,
StudentExamsComponent,
StudentsExamImportDialogComponent,
StudentsExamImportButtonComponent,
StudentExamDetailComponent,
DurationPipe,
],
Expand Down
9 changes: 8 additions & 1 deletion src/main/webapp/app/exam/manage/exam-management.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ export class ExamResolve implements Resolve<Exam> {
resolve(route: ActivatedRouteSnapshot): Observable<Exam> {
const courseId = route.params['courseId'] ? route.params['courseId'] : null;
const examId = route.params['examId'] ? route.params['examId'] : null;
const withStudents = route.data['requestOptions'] ? route.data['requestOptions'].withStudents : false;
if (courseId && examId) {
return this.examManagementService.find(courseId, examId).pipe(
return this.examManagementService.find(courseId, examId, withStudents).pipe(
filter((response: HttpResponse<Exam>) => response.ok),
map((exam: HttpResponse<Exam>) => exam.body!),
);
Expand Down Expand Up @@ -186,9 +187,15 @@ export const examManagementRoute: Routes = [
{
path: ':examId/students',
component: ExamStudentsComponent,
resolve: {
exam: ExamResolve,
},
data: {
authorities: ['ROLE_INSTRUCTOR', 'ROLE_ADMIN'],
pageTitle: 'artemisApp.examManagement.title',
requestOptions: {
withStudents: true,
},
},
canActivate: [UserRouteAccessService],
},
Expand Down
38 changes: 36 additions & 2 deletions src/main/webapp/app/exam/manage/exam-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as moment from 'moment';
import { SERVER_API_URL } from 'app/app.constants';
import { Exam } from 'app/entities/exam.model';
import { createRequestOption } from 'app/shared/util/request-util';
import { StudentDTO } from 'app/entities/student-dto.model';

type EntityResponseType = HttpResponse<Exam>;
type EntityArrayResponseType = HttpResponse<Exam[]>;
Expand Down Expand Up @@ -45,10 +46,12 @@ export class ExamManagementService {
* Find an exam on the server using a GET request.
* @param courseId The course id.
* @param examId The id of the exam to get.
* @param withStudents Boolean flag whether to fetch all students registered for the exam
*/
find(courseId: number, examId: number): Observable<EntityResponseType> {
find(courseId: number, examId: number, withStudents = false): Observable<EntityResponseType> {
const options = createRequestOption({ withStudents });
return this.http
.get<Exam>(`${this.resourceUrl}/${courseId}/exams/${examId}`, { observe: 'response' })
.get<Exam>(`${this.resourceUrl}/${courseId}/exams/${examId}`, { params: options, observe: 'response' })
.pipe(map((res: EntityResponseType) => ExamManagementService.convertDateFromServer(res)));
}

Expand Down Expand Up @@ -83,6 +86,37 @@ export class ExamManagementService {
return this.http.delete<any>(`${this.resourceUrl}/${courseId}/exams/${examId}`, { observe: 'response' });
}

/**
* Add a student to the registered users for an exam
* @param courseId The course id.
* @param examId The id of the exam to which to add the student
* @param studentLogin Login of the student
*/
addStudentToExam(courseId: number, examId: number, studentLogin: string): Observable<HttpResponse<any>> {
return this.http.post<any>(`${this.resourceUrl}/${courseId}/exams/${examId}/students/${studentLogin}`, { observe: 'response' });
}

/**
* Add students to the registered users for an exam
* @param courseId The course id.
* @param examId The id of the exam to which to add the student
* @param studentDtos Student DTOs of student to add to the exam
* @return studentDtos of students that were not found in the system
*/
addStudentsToExam(courseId: number, examId: number, studentDtos: StudentDTO[]): Observable<HttpResponse<StudentDTO[]>> {
return this.http.post<any>(`${this.resourceUrl}/${courseId}/exams/${examId}/students`, studentDtos, { observe: 'response' });
}

/**
* Remove a student to the registered users for an exam
* @param courseId The course id.
* @param examId The id of the exam from which to remove the student
* @param studentLogin Login of the student
*/
removeStudentFromExam(courseId: number, examId: number, studentLogin: string): Observable<HttpResponse<any>> {
return this.http.delete<any>(`${this.resourceUrl}/${courseId}/exams/${examId}/students/${studentLogin}`, { observe: 'response' });
}

private static convertDateFromClient(exam: Exam): Exam {
return Object.assign({}, exam, {
startDate: exam.startDate && moment(exam.startDate).isValid() ? exam.startDate.toJSON() : null,
Expand Down
145 changes: 144 additions & 1 deletion src/main/webapp/app/exam/manage/students/exam-students.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,144 @@
TODO
<div>
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
<h3 class="mb-0">
<span jhiTranslate="artemisApp.examManagement.students">Students</span>
<span class="text-muted">({{ exam.title }})</span>
</h3>
<div>
<span [jhiTranslate]="'artemisApp.examManagement.examStudents.registeredStudents'">Registered students</span><span>: {{ allRegisteredUsers.length }}</span>
<span *ngIf="filteredUsersSize < allRegisteredUsers.length" class="text-muted">
(<span [jhiTranslate]="'artemisApp.examManagement.examStudents.searchResults'">search results</span>: {{ filteredUsersSize }})
</span>
<jhi-students-exam-import-button
class="ml-4"
[courseId]="courseId"
[exam]="exam"
[buttonSize]="ButtonSize.MEDIUM"
(finish)="reloadExamWithRegisterdUsers()"
></jhi-students-exam-import-button>
</div>
</div>
<jhi-alert></jhi-alert>
<jhi-data-table
[isLoading]="isLoading"
[isSearching]="isSearching"
[searchFailed]="searchFailed"
[searchNoResults]="searchNoResults"
[isTransitioning]="isTransitioning"
entityType="user"
[allEntities]="allRegisteredUsers"
entitiesPerPageTranslation="artemisApp.examManagement.examStudents.usersPerPage"
showAllEntitiesTranslation="artemisApp.examManagement.examStudents.showAllUsers"
searchNoResultsTranslation="artemisApp.examManagement.examStudents.searchNoResults"
searchPlaceholderTranslation="artemisApp.examManagement.examStudents.searchForUsers"
[searchFields]="['login', 'name']"
[searchTextFromEntity]="searchTextFromUser"
[searchResultFormatter]="searchResultFormatter"
[onSearchWrapper]="searchAllUsers"
[onAutocompleteSelectWrapper]="onAutocompleteSelect"
(entitiesSizeChange)="handleUsersSizeChange($event)"
>
<ng-template let-settings="settings" let-controls="controls">
<ngx-datatable
class="bootstrap"
[limit]="settings.limit"
[sortType]="settings.sortType"
[columnMode]="settings.columnMode"
[headerHeight]="settings.headerHeight"
[footerHeight]="settings.footerHeight"
[rowHeight]="settings.rowHeight"
[rows]="settings.rows"
[rowClass]="dataTableRowClass"
[scrollbarH]="settings.scrollbarH"
>
<ngx-datatable-column prop="" [minWidth]="60" [width]="80" [maxWidth]="100">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('id')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="global.field.id">
ID
</span>
<fa-icon [icon]="controls.iconForSortPropField('id')"></fa-icon>
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<a routerLink="/admin/user-management/{{ value?.login }}/view">{{ value.id }}</a>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="" [minWidth]="150" [width]="200" [maxWidth]="200">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('login')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.examManagement.examStudents.login">
Login
</span>
<fa-icon [icon]="controls.iconForSortPropField('login')"></fa-icon>
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<a routerLink="/admin/user-management/{{ value?.login }}/view">{{ value.login }}</a>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="name" [minWidth]="150" [width]="250" [maxWidth]="250">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('name')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.examManagement.examStudents.name">
Name
</span>
<fa-icon [icon]="controls.iconForSortPropField('name')"></fa-icon>
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<span>{{ value }}</span>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="email" [minWidth]="150" [width]="250" [maxWidth]="250">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('email')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.examManagement.examStudents.email">
Email
</span>
<fa-icon [icon]="controls.iconForSortPropField('email')"></fa-icon>
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<span>{{ value }}</span>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="visibleRegistrationNumber" [minWidth]="150" [width]="250">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('visibleRegistrationNumber')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.examManagement.examStudents.registrationNumber">
Email
</span>
<fa-icon [icon]="controls.iconForSortPropField('visibleRegistrationNumber')"></fa-icon>
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<span>{{ value }}</span>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="" [minWidth]="150" [width]="200">
<ng-template ngx-datatable-header-template></ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<div class="w-100 text-right">
<button
jhiDeleteButton
[actionType]="ActionType.Remove"
[entityTitle]="value.login"
deleteQuestion="artemisApp.examManagement.examStudents.removeFromExam.modalQuestion"
(delete)="removeFromExam(value)"
[dialogError]="dialogError$"
>
<fa-icon [icon]="'user-slash'" class="mr-1"></fa-icon>
</button>
</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</ng-template>
</jhi-data-table>
</div>
Loading

0 comments on commit f53e29a

Please sign in to comment.