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

Lectures: Add hide/show functionality to PDF Preview Component #9667

Draft
wants to merge 13 commits into
base: feature/lectures/pdf-preview-decomposition
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/lecture/domain/Slide.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.tum.cit.aet.artemis.lecture.domain;

import java.util.Date;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
Expand Down Expand Up @@ -27,6 +29,9 @@ public class Slide extends DomainObject {
@Column(name = "slide_number")
private int slideNumber;

@Column(name = "hidden")
private Date hidden;

public AttachmentUnit getAttachmentUnit() {
return attachmentUnit;
}
Expand All @@ -50,4 +55,12 @@ public int getSlideNumber() {
public void setSlideNumber(int slideNumber) {
this.slideNumber = slideNumber;
}

public Date getHidden() {
return hidden;
}

public void setHidden(Date hidden) {
this.hidden = hidden;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.List;

import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
Expand All @@ -17,4 +21,17 @@ public interface SlideRepository extends ArtemisJpaRepository<Slide, Long> {

Slide findSlideByAttachmentUnitIdAndSlideNumber(Long attachmentUnitId, Integer slideNumber);

/**
* Finds the list of hidden slide IDs for a specific attachment unit ID.
*
* @param attachmentUnitId The ID of the attachment unit.
* @return List of hidden slide IDs.
*/
@Query("""
SELECT s.slideNumber
FROM Slide s
WHERE s.attachmentUnit.id = :attachmentUnitId AND s.hidden IS NOT NULL
""")
List<Integer> findHiddenSlideNumbersByAttachmentUnitId(@Param("attachmentUnitId") Long attachmentUnitId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ public AttachmentUnit createAttachmentUnit(AttachmentUnit attachmentUnit, Attach
* @param updateAttachment The new attachment data.
* @param updateFile The optional file.
* @param keepFilename Whether to keep the original filename or not.
* @param hiddenPages The hidden pages of attachment unit.
* @return The updated attachment unit.
*/
public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit, AttachmentUnit updateUnit, Attachment updateAttachment, MultipartFile updateFile,
boolean keepFilename) {
boolean keepFilename, String hiddenPages) {
Set<CompetencyLectureUnitLink> existingCompetencyLinks = new HashSet<>(existingAttachmentUnit.getCompetencyLinks());

existingAttachmentUnit.setDescription(updateUnit.getDescription());
Expand Down Expand Up @@ -144,7 +145,7 @@ public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit
}
// Split the updated file into single slides only if it is a pdf
if (Objects.equals(FilenameUtils.getExtension(updateFile.getOriginalFilename()), "pdf")) {
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit);
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit, hiddenPages);
}
if (pyrisWebhookService.isPresent() && irisSettingsRepository.isPresent()) {
pyrisWebhookService.get().autoUpdateAttachmentUnitsInPyris(savedAttachmentUnit.getLecture().getCourse().getId(), List.of(savedAttachmentUnit));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public LectureUnit importLectureUnit(final LectureUnit importedLectureUnit) {
attachment.setAttachmentUnit(attachmentUnit);
attachmentRepository.save(attachment);
if (attachment.getLink().endsWith(".pdf")) {
slideSplitterService.splitAttachmentUnitIntoSingleSlides(attachmentUnit);
slideSplitterService.splitAttachmentUnitIntoSingleSlides(attachmentUnit, null);
}
attachmentUnit.setAttachment(attachment);
return attachmentUnit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public List<AttachmentUnit> splitAndSaveUnits(LectureUnitSplitInformationDTO lec

MultipartFile multipartFile = fileService.convertByteArrayToMultipart(lectureUnit.unitName(), ".pdf", outputStream.toByteArray());
AttachmentUnit savedAttachmentUnit = attachmentUnitService.createAttachmentUnit(attachmentUnit, attachment, lecture, multipartFile, true);
slideSplitterService.splitAttachmentUnitIntoSingleSlides(documentUnits.getFirst(), savedAttachmentUnit, multipartFile.getOriginalFilename());
slideSplitterService.splitAttachmentUnitIntoSingleSlides(documentUnits.getFirst(), savedAttachmentUnit, multipartFile.getOriginalFilename(), null);
documentUnits.getFirst().close(); // make sure to close the document
units.add(savedAttachmentUnit);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import javax.imageio.ImageIO;

Expand Down Expand Up @@ -39,6 +44,8 @@ public class SlideSplitterService {

private static final Logger log = LoggerFactory.getLogger(SlideSplitterService.class);

static final LocalDate FOREVER = LocalDate.of(9999, 12, 31);

private final FileService fileService;

private final SlideRepository slideRepository;
Expand All @@ -52,14 +59,15 @@ public SlideSplitterService(FileService fileService, SlideRepository slideReposi
* Splits an Attachment Unit file into single slides and saves them as PNG files asynchronously.
*
* @param attachmentUnit The attachment unit to which the slides belong.
* @param hiddenPages The hidden pages of the attachment unit.
*/
@Async
public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit) {
public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit, String hiddenPages) {
Path attachmentPath = FilePathService.actualPathForPublicPath(URI.create(attachmentUnit.getAttachment().getLink()));
File file = attachmentPath.toFile();
try (PDDocument document = Loader.loadPDF(file)) {
String pdfFilename = file.getName();
splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename);
splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename, hiddenPages);
}
catch (IOException e) {
log.error("Error while splitting Attachment Unit {} into single slides", attachmentUnit.getId(), e);
Expand All @@ -68,19 +76,23 @@ public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit) {
}

/**
* Splits an Attachment Unit file into single slides and saves them as PNG files. Document is closed where this method is called.
* Splits an Attachment Unit file into single slides and saves them as PNG files or updates existing slides.
*
* @param attachmentUnit The attachment unit to which the slides belong.
* @param document The PDF document that is already loaded.
* @param pdfFilename The name of the PDF file.
* @param hiddenPages The hidden pages of the attachment unit.
*/
public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentUnit attachmentUnit, String pdfFilename) {
public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentUnit attachmentUnit, String pdfFilename, String hiddenPages) {
log.debug("Splitting Attachment Unit file {} into single slides", attachmentUnit.getAttachment().getName());
try {
String fileNameWithOutExt = FilenameUtils.removeExtension(pdfFilename);
int numPages = document.getNumberOfPages();
PDFRenderer pdfRenderer = new PDFRenderer(document);

List<Integer> hiddenPagesList = hiddenPages != null && !hiddenPages.isEmpty() ? Arrays.stream(hiddenPages.split(",")).map(Integer::parseInt).toList()
: Collections.emptyList();

for (int page = 0; page < numPages; page++) {
BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(page, 72, ImageType.RGB);
byte[] imageInByte = bufferedImageToByteArray(bufferedImage, "png");
Expand All @@ -89,11 +101,15 @@ public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentU
MultipartFile slideFile = fileService.convertByteArrayToMultipart(filename, ".png", imageInByte);
Path savePath = fileService.saveFile(slideFile, FilePathService.getAttachmentUnitFilePath().resolve(attachmentUnit.getId().toString()).resolve("slide")
.resolve(String.valueOf(slideNumber)).resolve(filename));
Slide slideEntity = new Slide();
slideEntity.setSlideImagePath(FilePathService.publicPathForActualPath(savePath, (long) slideNumber).toString());
slideEntity.setSlideNumber(slideNumber);
slideEntity.setAttachmentUnit(attachmentUnit);
slideRepository.save(slideEntity);

Optional<Slide> existingSlideOpt = Optional.ofNullable(slideRepository.findSlideByAttachmentUnitIdAndSlideNumber(attachmentUnit.getId(), slideNumber));
Slide slide = existingSlideOpt.orElseGet(Slide::new);
slide.setSlideImagePath(FilePathService.publicPathForActualPath(savePath, (long) slideNumber).toString());
slide.setSlideNumber(slideNumber);
slide.setAttachmentUnit(attachmentUnit);
slide.setHidden(hiddenPagesList.contains(slideNumber) ? java.sql.Date.valueOf(FOREVER) : null);
slideRepository.save(slide);

}
}
catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import de.tum.cit.aet.artemis.lecture.dto.LectureUnitSplitInformationDTO;
import de.tum.cit.aet.artemis.lecture.repository.AttachmentUnitRepository;
import de.tum.cit.aet.artemis.lecture.repository.LectureRepository;
import de.tum.cit.aet.artemis.lecture.repository.SlideRepository;
import de.tum.cit.aet.artemis.lecture.service.AttachmentUnitService;
import de.tum.cit.aet.artemis.lecture.service.LectureUnitProcessingService;
import de.tum.cit.aet.artemis.lecture.service.SlideSplitterService;
Expand Down Expand Up @@ -75,9 +76,11 @@ public class AttachmentUnitResource {

private final FileService fileService;

private final SlideRepository slideRepository;

public AttachmentUnitResource(AttachmentUnitRepository attachmentUnitRepository, LectureRepository lectureRepository, LectureUnitProcessingService lectureUnitProcessingService,
AuthorizationCheckService authorizationCheckService, GroupNotificationService groupNotificationService, AttachmentUnitService attachmentUnitService,
CompetencyProgressService competencyProgressService, SlideSplitterService slideSplitterService, FileService fileService) {
CompetencyProgressService competencyProgressService, SlideSplitterService slideSplitterService, FileService fileService, SlideRepository slideRepository) {
this.attachmentUnitRepository = attachmentUnitRepository;
this.lectureUnitProcessingService = lectureUnitProcessingService;
this.lectureRepository = lectureRepository;
Expand All @@ -87,6 +90,7 @@ public AttachmentUnitResource(AttachmentUnitRepository attachmentUnitRepository,
this.competencyProgressService = competencyProgressService;
this.slideSplitterService = slideSplitterService;
this.fileService = fileService;
this.slideRepository = slideRepository;
}

/**
Expand Down Expand Up @@ -117,19 +121,20 @@ public ResponseEntity<AttachmentUnit> getAttachmentUnit(@PathVariable Long attac
* @param file the optional file to upload
* @param keepFilename specifies if the original filename should be kept or not
* @param notificationText the text to be used for the notification. No notification will be sent if the parameter is not set
* @param hiddenPages the pages to be hidden in the attachment unit. No hidden pages will be sent if the parameter is not set
* @return the ResponseEntity with status 200 (OK) and with body the updated attachmentUnit
*/
@PutMapping(value = "lectures/{lectureId}/attachment-units/{attachmentUnitId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@EnforceAtLeastEditor
public ResponseEntity<AttachmentUnit> updateAttachmentUnit(@PathVariable Long lectureId, @PathVariable Long attachmentUnitId, @RequestPart AttachmentUnit attachmentUnit,
@RequestPart Attachment attachment, @RequestPart(required = false) MultipartFile file, @RequestParam(defaultValue = "false") boolean keepFilename,
@RequestParam(value = "notificationText", required = false) String notificationText) {
@RequestParam(value = "notificationText", required = false) String notificationText, @RequestParam(value = "hiddenPages", required = false) String hiddenPages) {
log.debug("REST request to update an attachment unit : {}", attachmentUnit);
AttachmentUnit existingAttachmentUnit = attachmentUnitRepository.findOneWithSlidesAndCompetencies(attachmentUnitId);
checkAttachmentUnitCourseAndLecture(existingAttachmentUnit, lectureId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, existingAttachmentUnit.getLecture().getCourse(), null);

AttachmentUnit savedAttachmentUnit = attachmentUnitService.updateAttachmentUnit(existingAttachmentUnit, attachmentUnit, attachment, file, keepFilename);
AttachmentUnit savedAttachmentUnit = attachmentUnitService.updateAttachmentUnit(existingAttachmentUnit, attachmentUnit, attachment, file, keepFilename, hiddenPages);

if (notificationText != null) {
groupNotificationService.notifyStudentGroupAboutAttachmentChange(savedAttachmentUnit.getAttachment(), notificationText);
Expand Down Expand Up @@ -170,7 +175,7 @@ public ResponseEntity<AttachmentUnit> createAttachmentUnit(@PathVariable Long le
AttachmentUnit savedAttachmentUnit = attachmentUnitService.createAttachmentUnit(attachmentUnit, attachment, lecture, file, keepFilename);
lectureRepository.save(lecture);
if (Objects.equals(FilenameUtils.getExtension(file.getOriginalFilename()), "pdf")) {
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit);
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit, null);
}
attachmentUnitService.prepareAttachmentUnitForClient(savedAttachmentUnit);
competencyProgressService.updateProgressByLearningObjectAsync(savedAttachmentUnit);
Expand Down Expand Up @@ -332,4 +337,16 @@ private void checkFile(Path filePath) {
throw new BadRequestAlertException("The file must be a pdf", ENTITY_NAME, "wrongFileType");
}
}

@GetMapping("lectures/{lectureId}/attachment-units/{attachmentUnitId}/hidden-slides")
@EnforceAtLeastEditor
public ResponseEntity<List<Integer>> getAttachmentUnitHiddenSlides(@PathVariable Long attachmentUnitId, @PathVariable Long lectureId) {
log.debug("REST request to get hidden slides of AttachmentUnit : {}", attachmentUnitId);
List<Integer> hiddenSlides = slideRepository.findHiddenSlideNumbersByAttachmentUnitId(attachmentUnitId);
AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId);
checkAttachmentUnitCourseAndLecture(attachmentUnit, lectureId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, attachmentUnit.getLecture().getCourse(), null);

return ResponseEntity.ok().body(hiddenSlides);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<addColumn tableName="slide">
<column name="hidden" type="datetime(3)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +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"/>

<!-- 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export class AttachmentUnitService {
.pipe(map((res: EntityResponseType) => this.lectureUnitService.convertLectureUnitResponseDatesFromServer(res)));
}

update(lectureId: number, attachmentUnitId: number, formData: FormData, notificationText?: string): Observable<EntityResponseType> {
update(lectureId: number, attachmentUnitId: number, formData: FormData, notificationText?: string, hiddenPages?: string): Observable<EntityResponseType> {
return this.httpClient
.put<AttachmentUnit>(
`${this.resourceURL}/lectures/${lectureId}/attachment-units/${attachmentUnitId}?keepFilename=true` +
(notificationText ? `&notificationText=${notificationText}` : ''),
(notificationText ? `&notificationText=${notificationText}` : '') +
(hiddenPages ? `&hiddenPages=${hiddenPages}` : ''),
formData,
{ observe: 'response' },
)
Expand Down Expand Up @@ -71,4 +72,15 @@ export class AttachmentUnitService {
getAttachmentFile(courseId: number, attachmentUnitId: number): Observable<Blob> {
return this.httpClient.get(`${this.resourceURL}/files/courses/${courseId}/attachment-units/${attachmentUnitId}`, { responseType: 'blob' });
}

/**
* Retrieve the hidden slide numbers associated with a given attachment unit ID.
*
* @param lectureId The ID of the lecture that the Attachment Unit belongs to.
* @param attachmentUnitId The ID of the attachment unit.
* @returns An Observable that emits a list of integers representing hidden slide numbers.
*/
getHiddenSlides(lectureId: number, attachmentUnitId: number): Observable<number[]> {
return this.httpClient.get<number[]>(`${this.resourceURL}/lectures/${lectureId}/attachment-units/${attachmentUnitId}/hidden-slides`);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
<div class="pdf-container" #pdfContainer>
@if (isEnlargedView()) {
<jhi-pdf-preview-enlarged-canvas-component
Expand Down
Loading
Loading