Skip to content

Commit

Permalink
Lectures: Add PDF preview for instructors (#8987)
Browse files Browse the repository at this point in the history
  • Loading branch information
eceeeren authored and kaancayli committed Sep 5, 2024
1 parent f60288b commit 4c08cc7
Show file tree
Hide file tree
Showing 25 changed files with 1,702 additions and 30 deletions.
453 changes: 432 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"ngx-infinite-scroll": "18.0.0",
"ngx-webstorage": "18.0.0",
"papaparse": "5.4.1",
"pdfjs-dist": "^4.5.136",
"posthog-js": "1.160.0",
"rxjs": "7.8.1",
"showdown": "2.1.0",
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor;
import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse;
import de.tum.in.www1.artemis.service.AuthorizationCheckService;
import de.tum.in.www1.artemis.service.FilePathService;
import de.tum.in.www1.artemis.service.FileService;
Expand Down Expand Up @@ -372,6 +373,24 @@ public ResponseEntity<byte[]> getLectureAttachment(@PathVariable Long lectureId,
return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false);
}

/**
* GET /files/courses/{courseId}/attachments/{attachmentId} : Returns the file associated with the
* given attachment ID as a downloadable resource
*
* @param courseId The ID of the course that the Attachment belongs to
* @param attachmentId the ID of the attachment to retrieve
* @return ResponseEntity containing the file as a resource
*/
@GetMapping("files/courses/{courseId}/attachments/{attachmentId}")
@EnforceAtLeastEditorInCourse
public ResponseEntity<byte[]> getAttachmentFile(@PathVariable Long courseId, @PathVariable Long attachmentId) {
Attachment attachment = attachmentRepository.findByIdElseThrow(attachmentId);
Course course = courseRepository.findByIdElseThrow(courseId);
checkAttachmentExistsInCourseOrThrow(course, attachment);

return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false);
}

/**
* GET /files/attachments/lecture/{lectureId}/merge-pdf : Get the lecture units
* PDF attachments merged
Expand Down Expand Up @@ -428,6 +447,26 @@ public ResponseEntity<byte[]> getAttachmentUnitAttachment(@PathVariable Long att
return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false);
}

/**
* GET files/courses/{courseId}/attachment-units/{attachmenUnitId} : Returns the file associated with the
* given attachmentUnit ID as a downloadable resource
*
* @param courseId The ID of the course that the Attachment belongs to
* @param attachmentUnitId the ID of the attachment to retrieve
* @return ResponseEntity containing the file as a resource
*/
@GetMapping("files/courses/{courseId}/attachment-units/{attachmentUnitId}")
@EnforceAtLeastEditorInCourse
public ResponseEntity<byte[]> getAttachmentUnitFile(@PathVariable Long courseId, @PathVariable Long attachmentUnitId) {
log.debug("REST request to get file for attachment unit : {}", attachmentUnitId);
AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId);
Course course = courseRepository.findByIdElseThrow(courseId);
Attachment attachment = attachmentUnit.getAttachment();
checkAttachmentUnitExistsInCourseOrThrow(course, attachmentUnit);

return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false);
}

/**
* GET files/attachments/slides/attachment-unit/:attachmentUnitId/slide/:slideNumber : Get the lecture unit attachment slide by slide number
*
Expand Down Expand Up @@ -557,6 +596,30 @@ private void checkAttachmentAuthorizationOrThrow(Course course, Attachment attac
}
}

/**
* Checks if the attachment exists in the mentioned course
*
* @param course the course to check if the attachment is part of it
* @param attachment the attachment for which the existence should be checked
*/
private void checkAttachmentExistsInCourseOrThrow(Course course, Attachment attachment) {
if (!attachment.getLecture().getCourse().equals(course)) {
throw new EntityNotFoundException("This attachment does not exist in this course.");
}
}

/**
* Checks if the attachment exists in the mentioned course
*
* @param course the course to check if the attachment is part of it
* @param attachmentUnit the attachment unit for which the existence should be checked
*/
private void checkAttachmentUnitExistsInCourseOrThrow(Course course, AttachmentUnit attachmentUnit) {
if (!attachmentUnit.getLecture().getCourse().equals(course)) {
throw new EntityNotFoundException("This attachment unit does not exist in this course.");
}
}

/**
* Reads the file and turns it into a ResponseEntity
*
Expand Down
11 changes: 11 additions & 0 deletions src/main/webapp/app/lecture/attachment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,15 @@ export class AttachmentService {
}
return formData;
}

/**
* Retrieve the file associated with a given attachment ID as a Blob object
*
* @param courseId The ID of the course that the attachment belongs to
* @param attachmentId The ID of the attachment to retrieve
* @returns An Observable that emits the Blob object of the file when the HTTP request completes successfully
*/
getAttachmentFile(courseId: number, attachmentId: number): Observable<Blob> {
return this.http.get(`api/files/courses/${courseId}/attachments/${attachmentId}`, { responseType: 'blob' });
}
}
11 changes: 11 additions & 0 deletions src/main/webapp/app/lecture/lecture-attachments.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ <h4 jhiTranslate="artemisApp.lecture.attachments.attachments"></h4>
</td>
<td class="text-end">
<div class="flex-btn-group-container">
@if (viewButtonAvailable[attachment.id!]) {
<button
type="button"
class="btn btn-primary btn-sm me-1"
[routerLink]="['/course-management', lecture.course?.id, 'lectures', lecture.id, 'attachments', attachment.id]"
[ngbTooltip]="'entity.action.view' | artemisTranslate"
>
<fa-icon [icon]="faEye" />
<span class="d-none d-md-inline" jhiTranslate="entity.action.view"></span>
</button>
}
<button
[disabled]="attachmentToBeCreated?.id === attachment?.id"
type="submit"
Expand Down
15 changes: 14 additions & 1 deletion src/main/webapp/app/lecture/lecture-attachments.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Subject } from 'rxjs';
import { FileService } from 'app/shared/http/file.service';
import { Attachment, AttachmentType } from 'app/entities/attachment.model';
import { AttachmentService } from 'app/lecture/attachment.service';
import { faPaperclip, faPencilAlt, faQuestionCircle, faSpinner, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons';
import { faEye, faPaperclip, faPencilAlt, faQuestionCircle, faSpinner, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FILE_EXTENSIONS } from 'app/shared/constants/file-extensions.constants';
import { LectureService } from 'app/lecture/lecture.service';

Expand All @@ -31,6 +31,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy {
notificationText?: string;
erroredFile?: File;
errorMessage?: string;
viewButtonAvailable: Record<number, boolean> = {};

// A human-readable list of allowed file extensions
readonly allowedFileExtensions = FILE_EXTENSIONS.join(', ');
Expand All @@ -47,6 +48,7 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy {
faPencilAlt = faPencilAlt;
faPaperclip = faPaperclip;
faQuestionCircle = faQuestionCircle;
faEye = faEye;

constructor(
protected activatedRoute: ActivatedRoute,
Expand Down Expand Up @@ -75,13 +77,20 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy {
loadAttachments(): void {
this.attachmentService.findAllByLectureId(this.lecture.id!).subscribe((attachmentsResponse: HttpResponse<Attachment[]>) => {
this.attachments = attachmentsResponse.body!;
this.attachments.forEach((attachment) => {
this.viewButtonAvailable[attachment.id!] = this.isViewButtonAvailable(attachment.link!);
});
});
}

ngOnDestroy(): void {
this.dialogErrorSource.unsubscribe();
}

isViewButtonAvailable(attachmentLink: string): boolean {
return attachmentLink.endsWith('.pdf') ?? false;
}

get isSubmitPossible(): boolean {
return !!(this.attachmentToBeCreated?.name && (this.attachmentFile || this.attachmentToBeCreated?.link));
}
Expand Down Expand Up @@ -130,9 +139,13 @@ export class LectureAttachmentsComponent implements OnInit, OnDestroy {
this.attachmentService.create(this.attachmentToBeCreated!, this.attachmentFile!).subscribe({
next: (attachmentRes: HttpResponse<Attachment>) => {
this.attachments.push(attachmentRes.body!);
this.lectureService.findWithDetails(this.lecture.id!).subscribe((lectureResponse: HttpResponse<Lecture>) => {
this.lecture = lectureResponse.body!;
});
this.attachmentFile = undefined;
this.attachmentToBeCreated = undefined;
this.attachmentBackup = undefined;
this.loadAttachments();
},
error: (error: HttpErrorResponse) => this.handleFailedUpload(error),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,15 @@ export class AttachmentUnitService {
const params = new HttpParams().set('commaSeparatedKeyPhrases', keyPhrases);
return this.httpClient.get<Array<number>>(`${this.resourceURL}/lectures/${lectureId}/attachment-units/slides-to-remove/${filename}`, { params, observe: 'response' });
}

/**
* Retrieve the file associated with a given attachment ID as a Blob object
*
* @param courseId The ID of the course that the Attachment Unit belongs to
* @param attachmentUnitId The ID of the attachment to retrieve
* @returns An Observable that emits the Blob object of the file when the HTTP request completes successfully
*/
getAttachmentFile(courseId: number, attachmentUnitId: number): Observable<Blob> {
return this.httpClient.get(`${this.resourceURL}/files/courses/${courseId}/attachment-units/${attachmentUnitId}`, { responseType: 'blob' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@
@if (lecture.course?.id && showCompetencies) {
<jhi-competencies-popover [courseId]="lecture.course!.id!" [competencies]="lectureUnit.competencies || []" [navigateTo]="'competencyManagement'" />
}
@if (viewButtonAvailable[lectureUnit.id!]) {
<a
type="button"
class="btn btn-primary btn-sm flex-grow-1"
[routerLink]="['/course-management', lecture.course?.id, 'lectures', lecture.id, 'unit-management', 'attachment-units', lectureUnit.id, 'view']"
[ngbTooltip]="'entity.action.view' | artemisTranslate"
>
<fa-icon [icon]="faEye" />
</a>
}
<div class="d-flex gap-1 w-100">
@if (this.emitEditEvents) {
@if (editButtonAvailable(lectureUnit)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-manage
import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model';
import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model';
import { ExerciseUnit } from 'app/entities/lecture-unit/exerciseUnit.model';
import { faPencilAlt, faTrash } from '@fortawesome/free-solid-svg-icons';
import { faEye, faPencilAlt, faTrash } from '@fortawesome/free-solid-svg-icons';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';

@Component({
Expand All @@ -34,8 +34,11 @@ export class LectureUnitManagementComponent implements OnInit, OnDestroy {
lecture: Lecture;
isLoading = false;
updateOrderSubject: Subject<any>;
viewButtonAvailable: Record<number, boolean> = {};

updateOrderSubjectSubscription: Subscription;
navigationEndSubscription: Subscription;

readonly LectureUnitType = LectureUnitType;
readonly ActionType = ActionType;
private dialogErrorSource = new Subject<string>();
Expand All @@ -51,6 +54,7 @@ export class LectureUnitManagementComponent implements OnInit, OnDestroy {
// Icons
faTrash = faTrash;
faPencilAlt = faPencilAlt;
faEye = faEye;

constructor(
private activatedRoute: ActivatedRoute,
Expand Down Expand Up @@ -104,6 +108,9 @@ export class LectureUnitManagementComponent implements OnInit, OnDestroy {
this.lecture = lecture;
if (lecture?.lectureUnits) {
this.lectureUnits = lecture?.lectureUnits;
this.lectureUnits.forEach((lectureUnit) => {
this.viewButtonAvailable[lectureUnit.id!] = this.isViewButtonAvailable(lectureUnit);
});
} else {
this.lectureUnits = [];
}
Expand Down Expand Up @@ -186,6 +193,17 @@ export class LectureUnitManagementComponent implements OnInit, OnDestroy {
});
}

isViewButtonAvailable(lectureUnit: LectureUnit): boolean {
switch (lectureUnit!.type) {
case LectureUnitType.ATTACHMENT: {
const attachmentUnit = <AttachmentUnit>lectureUnit;
return attachmentUnit.attachment?.link?.endsWith('.pdf') ?? false;
}
default:
return false;
}
}

editButtonAvailable(lectureUnit: LectureUnit) {
switch (lectureUnit?.type) {
case LectureUnitType.ATTACHMENT:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Routes } from '@angular/router';
import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, Routes } from '@angular/router';
import { Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { Authority } from 'app/shared/constants/authority.constants';
import { UserRouteAccessService } from 'app/core/auth/user-route-access-service';
import { LectureUnitManagementComponent } from 'app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component';
Expand All @@ -12,6 +16,27 @@ import { EditVideoUnitComponent } from 'app/lecture/lecture-unit/lecture-unit-ma
import { CreateOnlineUnitComponent } from 'app/lecture/lecture-unit/lecture-unit-management/create-online-unit/create-online-unit.component';
import { EditOnlineUnitComponent } from 'app/lecture/lecture-unit/lecture-unit-management/edit-online-unit/edit-online-unit.component';
import { AttachmentUnitsComponent } from 'app/lecture/lecture-unit/lecture-unit-management/attachment-units/attachment-units.component';
import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component';
import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model';
import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service';
import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service';

@Injectable({ providedIn: 'root' })
export class AttachmentUnitResolve implements Resolve<AttachmentUnit> {
constructor(private attachmentUnitService: AttachmentUnitService) {}

resolve(route: ActivatedRouteSnapshot): Observable<AttachmentUnit> {
const lectureId = route.params['lectureId'];
const attachmentUnitId = route.params['attachmentUnitId'];
if (attachmentUnitId) {
return this.attachmentUnitService.findById(attachmentUnitId, lectureId).pipe(
filter((response: HttpResponse<AttachmentUnit>) => response.ok),
map((attachmentUnit: HttpResponse<AttachmentUnit>) => attachmentUnit.body!),
);
}
return of(new AttachmentUnit());
}
}

export const lectureUnitRoute: Routes = [
{
Expand Down Expand Up @@ -86,6 +111,14 @@ export const lectureUnitRoute: Routes = [
pageTitle: 'artemisApp.attachmentUnit.editAttachmentUnit.title',
},
},
{
path: 'attachment-units/:attachmentUnitId/view',
component: PdfPreviewComponent,
resolve: {
course: CourseManagementResolve,
attachmentUnit: AttachmentUnitResolve,
},
},
{
path: 'video-units/:videoUnitId/edit',
component: EditVideoUnitComponent,
Expand Down
33 changes: 33 additions & 0 deletions src/main/webapp/app/lecture/lecture.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { Authority } from 'app/shared/constants/authority.constants';
import { lectureUnitRoute } from 'app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.route';
import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service';
import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component';
import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component';
import { Attachment } from 'app/entities/attachment.model';
import { AttachmentService } from 'app/lecture/attachment.service';

@Injectable({ providedIn: 'root' })
export class LectureResolve implements Resolve<Lecture> {
Expand All @@ -31,6 +34,22 @@ export class LectureResolve implements Resolve<Lecture> {
}
}

@Injectable({ providedIn: 'root' })
export class AttachmentResolve implements Resolve<Attachment> {
constructor(private attachmentService: AttachmentService) {}

resolve(route: ActivatedRouteSnapshot): Observable<Attachment> {
const attachmentId = route.params['attachmentId'];
if (attachmentId) {
return this.attachmentService.find(attachmentId).pipe(
filter((response: HttpResponse<Attachment>) => response.ok),
map((attachment: HttpResponse<Attachment>) => attachment.body!),
);
}
return of(new Attachment());
}
}

export const lectureRoute: Routes = [
{
path: ':courseId/lectures',
Expand Down Expand Up @@ -91,6 +110,20 @@ export const lectureRoute: Routes = [
},
canActivate: [UserRouteAccessService],
},
{
path: 'attachments',
canActivate: [UserRouteAccessService],
children: [
{
path: ':attachmentId',
component: PdfPreviewComponent,
resolve: {
attachment: AttachmentResolve,
course: CourseManagementResolve,
},
},
],
},
{
path: 'edit',
component: LectureUpdateComponent,
Expand Down
Loading

0 comments on commit 4c08cc7

Please sign in to comment.