Skip to content

Commit

Permalink
Change hidden file structure to hiddenLink
Browse files Browse the repository at this point in the history
  • Loading branch information
eceeeren committed Dec 11, 2024
1 parent 899e7d2 commit cc3bca2
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public class Attachment extends DomainObject implements Serializable {
@JoinColumn(name = "attachment_unit_id")
private AttachmentUnit attachmentUnit;

@Column(name = "hidden_link")
private String hiddenLink;

// jhipster-needle-entity-add-field - JHipster will add fields here, do not remove

public String getName() {
Expand Down Expand Up @@ -135,6 +138,14 @@ public void setAttachmentUnit(AttachmentUnit attachmentUnit) {
this.attachmentUnit = attachmentUnit;
}

public String getHiddenLink() {
return hiddenLink;
}

public void setHiddenLink(String hiddenLink) {
this.hiddenLink = hiddenLink;
}

public Boolean isVisibleToStudents() {
if (releaseDate == null) { // no release date means the attachment is visible to students
return Boolean.TRUE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,24 @@ public ResponseEntity<Attachment> createAttachment(@RequestPart Attachment attac
}

/**
* PUT /attachments/:id : Updates an existing attachment.
* PUT /attachments/:id : Updates an existing attachment, optionally handling both public and hidden files.
*
* @param attachmentId the id of the attachment to save
* @param attachment the attachment to update
* @param file the file to save if the file got changed (optional)
* @param notificationText text that will be sent to student group
* @param hiddenFile the file to add as hidden version of the attachment (optional)
* @param notificationText text that will be sent to the student group (optional)
* @return the ResponseEntity with status 200 (OK) and with body the updated attachment, or with status 400 (Bad Request) if the attachment is not valid, or with status 500
* (Internal Server Error) if the attachment couldn't be updated
*/
@PutMapping(value = "attachments/{attachmentId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@EnforceAtLeastEditor
public ResponseEntity<Attachment> updateAttachment(@PathVariable Long attachmentId, @RequestPart Attachment attachment, @RequestPart(required = false) MultipartFile file,
@RequestParam(value = "notificationText", required = false) String notificationText) {
@RequestPart(required = false) MultipartFile hiddenFile, @RequestParam(value = "notificationText", required = false) String notificationText) {
log.debug("REST request to update Attachment : {}", attachment);
attachment.setId(attachmentId);

// Make sure that the original references are preserved.
// Make sure that the original references are preserved
Attachment originalAttachment = attachmentRepository.findByIdOrElseThrow(attachment.getId());
attachment.setAttachmentUnit(originalAttachment.getAttachmentUnit());

Expand All @@ -131,6 +132,20 @@ public ResponseEntity<Attachment> updateAttachment(@PathVariable Long attachment
this.fileService.evictCacheForPath(FilePathService.actualPathForPublicPathOrThrow(oldPath));
}

if (hiddenFile != null) {
// Update hidden file logic
Path basePath = FilePathService.getAttachmentUnitFilePath().resolve(originalAttachment.getAttachmentUnit().getId().toString());
Path savePath = fileService.saveFile(hiddenFile, basePath, true);
attachment.setHiddenLink(FilePathService.publicPathForActualPath(savePath, originalAttachment.getAttachmentUnit().getId()).toString() + "v2");

// Delete the old hidden file
if (originalAttachment.getHiddenLink() != null) {
URI oldHiddenPath = URI.create(originalAttachment.getHiddenLink());
fileService.schedulePathForDeletion(FilePathService.actualPathForPublicPathOrThrow(oldHiddenPath), 0);
fileService.evictCacheForPath(FilePathService.actualPathForPublicPathOrThrow(oldHiddenPath));
}
}

Attachment result = attachmentRepository.save(attachment);
if (notificationText != null) {
groupNotificationService.notifyStudentGroupAboutAttachmentChange(result, notificationText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20241117234100" author="ece.eren">
<changeSet id="20241129170228" author="ece.eren">
<addColumn tableName="slide">
<column name="hidden" type="datetime(3)"/>
</addColumn>

<addColumn tableName="attachment">
<column name="hidden_link" type="varchar(255)" />
</addColumn>
</changeSet>
</databaseChangeLog>
2 changes: 1 addition & 1 deletion src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<include file="classpath:config/liquibase/changelog/20241010101010_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241018053210_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241023456789_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241117234100_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241129170228_changelog.xml" relativeToChangelogFile="false"/>

<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
<!-- we should also stay in a chronological order! -->
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/app/entities/attachment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class Attachment implements BaseEntity {
lecture?: Lecture;
exercise?: Exercise;
attachmentUnit?: AttachmentUnit;
hiddenLink?: string;

constructor() {}
}
10 changes: 7 additions & 3 deletions src/main/webapp/app/lecture/attachment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ export class AttachmentService {
* @param attachmentId the id of the attachment to update
* @param attachment the attachment object holding the updated values
* @param file the file to save as an attachment if it was changed (optional)
* @param hiddenFile the file to add as hidden version of the attachment (optional)
* @param req optional request parameters
*/
update(attachmentId: number, attachment: Attachment, file?: File, req?: any): Observable<EntityResponseType> {
update(attachmentId: number, attachment: Attachment, file?: File, hiddenFile?: File, req?: any): Observable<EntityResponseType> {
const options = createRequestOption(req);
const copy = this.convertAttachmentDatesFromClient(attachment);

return this.http
.put<Attachment>(this.resourceUrl + '/' + attachmentId, this.createFormData(copy, file), { params: options, observe: 'response' })
.put<Attachment>(this.resourceUrl + '/' + attachmentId, this.createFormData(copy, file, hiddenFile), { params: options, observe: 'response' })
.pipe(map((res: EntityResponseType) => this.convertAttachmentResponseDatesFromServer(res)));
}

Expand Down Expand Up @@ -128,12 +129,15 @@ export class AttachmentService {
return res;
}

private createFormData(attachment: Attachment, file?: File) {
private createFormData(attachment: Attachment, file?: File, hiddenFile?: File) {
const formData = new FormData();
formData.append('attachment', objectToJsonBlob(attachment));
if (file) {
formData.append('file', file);
}
if (hiddenFile) {
formData.append('hiddenFile', hiddenFile);
}
return formData;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ <h4>
</a>
</div>
<div>
<button type="submit" (click)="updateAttachmentWithFile()" class="btn btn-primary" [disabled]="isPdfLoading() || totalPages() === 0 || !isFileChanged()">
<button
type="submit"
(click)="updateAttachmentWithFile()"
class="btn btn-primary"
[disabled]="(isPdfLoading() || totalPages() === 0 || !isFileChanged()) && !hiddenPagesChanged()"
>
<fa-icon [icon]="faSave" [ngbTooltip]="'entity.action.save' | artemisTranslate"></fa-icon>
<span jhiTranslate="entity.action.save"></span>
</button>
Expand Down
98 changes: 79 additions & 19 deletions src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, ElementRef, OnChanges, OnDestroy, OnInit, SimpleChanges, computed, inject, signal, viewChild } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, computed, effect, inject, signal, viewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AttachmentService } from 'app/lecture/attachment.service';
import { Attachment } from 'app/entities/attachment.model';
Expand All @@ -17,6 +17,7 @@ import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants';
import { PdfPreviewThumbnailGridComponent } from 'app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component';
import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service';
import { PDFDocument } from 'pdf-lib';
import cloneDeep from 'lodash-es/cloneDeep';

@Component({
selector: 'jhi-pdf-preview-component',
Expand All @@ -25,7 +26,7 @@ import { PDFDocument } from 'pdf-lib';
standalone: true,
imports: [ArtemisSharedModule, PdfPreviewThumbnailGridComponent],
})
export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
export class PdfPreviewComponent implements OnInit, OnDestroy {
fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');

attachmentSub: Subscription;
Expand All @@ -44,6 +45,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
isFileChanged = signal<boolean>(false);
selectedPages = signal<Set<number>>(new Set());
allPagesSelected = computed(() => this.selectedPages().size === this.totalPages());
initialHiddenPages = signal<Set<number>>(new Set());
hiddenPages = signal<Set<number>>(new Set());

// Injected services
Expand Down Expand Up @@ -80,6 +82,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
this.attachmentUnit.set(data.attachmentUnit);
this.attachmentUnitService.getHiddenSlides(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!).subscribe({
next: (hiddenPages: number[]) => {
this.initialHiddenPages.set(new Set(hiddenPages));
this.hiddenPages.set(new Set(hiddenPages));
},
error: (error: HttpErrorResponse) => onError(this.alertService, error),
Expand All @@ -100,18 +103,36 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
this.attachmentUnitSub?.unsubscribe();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes['hiddenPages']) {
this.isFileChanged.set(true);
}
constructor() {
effect(
() => {
this.hiddenPagesChanged();
},
{ allowSignalWrites: true },
);
}

/**
* Triggers the file input to select files.
*/
triggerFileInput(): void {
this.fileInput().nativeElement.click();
}

/**
* Checks if there has been any change between the current set of hidden pages and the new set of hidden pages.
*
* @returns Returns true if the sets differ in size or if any element in `newHiddenPages` is not found in `hiddenPages`, otherwise false.
*/
hiddenPagesChanged() {
if (this.initialHiddenPages()!.size !== this.hiddenPages()!.size) return true;

for (const elem of this.initialHiddenPages()!) {
if (!this.hiddenPages()!.has(elem)) return true;
}
return false;
}

/**
* Retrieves an array of hidden page numbers from elements with IDs starting with "show-button-".
*
Expand All @@ -121,15 +142,15 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
return Array.from(document.querySelectorAll('.hide-show-btn.btn-success'))
.map((el) => {
const match = el.id.match(/hide-show-button-(\d+)/);
return match ? match[1] : null;
return match ? parseInt(match[1], 10) : null;
})
.filter((id) => id !== null);
}

/**
* Updates the existing attachment file or creates a new hidden version of the attachment.
*/
updateAttachmentWithFile(): void {
async updateAttachmentWithFile(): Promise<void> {
const pdfFile = new File([this.currentPdfBlob()!], 'updatedAttachment.pdf', { type: 'application/pdf' });

if (pdfFile.size > MAX_FILE_SIZE) {
Expand All @@ -152,25 +173,40 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
},
});
} else if (this.attachmentUnit()) {
const finalHiddenPages = this.getHiddenPages().join(',');
const finalHiddenPages = this.getHiddenPages();
this.attachmentToBeEdited.set(this.attachmentUnit()!.attachment!);
this.attachmentToBeEdited()!.version!++;
//this.attachmentToBeEdited()!.version!++;
this.attachmentToBeEdited()!.uploadDate = dayjs();

const formData = new FormData();
formData.append('file', pdfFile);
formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited()!));
formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit()!));

this.attachmentUnitService.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData, undefined, finalHiddenPages).subscribe({
next: async () => {
this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess');
this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']);
},
error: (error) => {
this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message });
},
});
if (this.hiddenPagesChanged()) {
const pdfFileWithHiddenPages = await this.createHiddenVersionOfAttachment(finalHiddenPages);
const attachmentWithHiddenPages = cloneDeep(this.attachmentToBeEdited()!);

this.attachmentService.update(attachmentWithHiddenPages.id!, attachmentWithHiddenPages, undefined, pdfFileWithHiddenPages).subscribe({
next: () => {
this.alertService.success('artemisApp.attachment.pdfPreview.hiddenAttachmentUpdateSuccess');
this.attachmentUnitService
.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData, undefined, finalHiddenPages.join(','))
.subscribe({
next: () => {
this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess');
this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']);
},
error: (error) => {
this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message });
},
});
},
error: (error) => {
this.alertService.error('artemisApp.attachment.pdfPreview.hiddenAttachmentUpdateError', { error: error.message });
},
});
}
}
}

Expand Down Expand Up @@ -255,6 +291,30 @@ export class PdfPreviewComponent implements OnInit, OnDestroy, OnChanges {
this.hiddenPages.set(updatedHiddenPages);
}

/**
* Creates a hidden version of the current PDF attachment by removing specified pages.
*
* @param hiddenPages - An array of page numbers to be removed from the original PDF.
* @returns A promise that resolves to a new `File` object representing the modified PDF, or undefined if an error occurs.
*/
async createHiddenVersionOfAttachment(hiddenPages: number[]) {
try {
const fileName = this.attachmentUnit()!.attachment!.name;
const existingPdfBytes = await this.currentPdfBlob()!.arrayBuffer();
const hiddenPdfDoc = await PDFDocument.load(existingPdfBytes);

const pagesToDelete = hiddenPages.map((page) => page - 1).sort((a, b) => b - a);
pagesToDelete.forEach((pageIndex) => {
hiddenPdfDoc.removePage(pageIndex);
});

const pdfBytes = await hiddenPdfDoc.save();
return new File([pdfBytes], `${fileName}.pdf`, { type: 'application/pdf' });
} catch (error) {
this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message });
}
}

/**
* Adds a selected PDF file at the end of the current PDF document.
* @param event - The event containing the file input.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ export class AttachmentUnitComponent extends LectureUnitDirective<AttachmentUnit
handleDownload() {
this.logEvent();

if (this.lectureUnit().attachment?.link) {
const link = this.lectureUnit().attachment!.link!;
this.fileService.downloadFile(this.fileService.replaceAttachmentPrefixAndUnderscores(link));
const link = this.lectureUnit().attachment?.hiddenLink || this.lectureUnit().attachment?.link; // Prefer hiddenLink if available
if (link) {
const sanitizedLink = this.fileService.replaceAttachmentPrefixAndUnderscores(link);
this.fileService.downloadFile(sanitizedLink);
this.onCompletion.emit({ lectureUnit: this.lectureUnit(), completed: true });
}
}
Expand Down

0 comments on commit cc3bca2

Please sign in to comment.