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: Allow drag and dropping of PDF pages #10457

Draft
wants to merge 31 commits into
base: feature/lectures/hide-pdf-pages-with-date
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a34b542
Drag and drop pages on client level
eceeeren Mar 9, 2025
d25cbfc
Change page order logic
eceeeren Mar 10, 2025
ae52188
Update selectedPages to work with OrderedPage
eceeeren Mar 10, 2025
626a8f2
Update getHiddenPages
eceeeren Mar 10, 2025
3800d36
Remove unnecessary code
eceeeren Mar 10, 2025
40e89fc
Small fixes
eceeeren Mar 11, 2025
21b47a8
Small fixes 2
eceeeren Mar 11, 2025
80df667
Move loadPdf to pdf preview
eceeeren Mar 11, 2025
95a39f0
Fix page appending
eceeeren Mar 11, 2025
8b67ff3
Create pages from proxies
eceeeren Mar 11, 2025
2821671
Fix enlarged canvas
eceeeren Mar 11, 2025
5c9c17a
Add new delete slides logic
eceeeren Mar 11, 2025
13c5d5f
Change pdf creation logic
eceeeren Mar 11, 2025
e1e87c9
Add hidden info to pageOrder
eceeeren Mar 11, 2025
3431123
Add showPages, remove toggleVisibility
eceeeren Mar 11, 2025
6eee100
Remove unnecessary functions
eceeeren Mar 11, 2025
c1215d4
Add hidePages function
eceeeren Mar 11, 2025
1c99479
Fix mergePdf logic
eceeeren Mar 11, 2025
c251797
Fix deleteSelectedPages logic
eceeeren Mar 11, 2025
4a1f89d
Add HiddenPage logic back
eceeeren Mar 11, 2025
0b98d9a
Small fixes
eceeeren Mar 11, 2025
e5a7620
Prevent rendering during reordering
eceeeren Mar 11, 2025
159a63f
Update slide updating method
eceeeren Mar 12, 2025
3ad1fca
Small fixes
eceeeren Mar 12, 2025
75192dc
Merge branch 'feature/lectures/hide-pdf-pages-with-date' into feature…
eceeeren Mar 13, 2025
c9b3c67
Fix DateBox client test
eceeeren Mar 13, 2025
ae9c735
Fix Thumbnail Grid client test
eceeeren Mar 13, 2025
c39ee7a
Small fixes
eceeeren Mar 13, 2025
3e432a0
Fix Pdf Preview client tests
eceeeren Mar 13, 2025
88f672a
Fix architecture test
eceeeren Mar 13, 2025
ce806fa
Remove standalone
eceeeren Mar 13, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
@Repository
public interface SlideRepository extends ArtemisJpaRepository<Slide, String> {

Slide findSlideByAttachmentUnitIdAndSlideNumber(Long attachmentUnitId, Integer slideNumber);

Slide findByAttachmentUnitIdAndId(Long attachmentUnitId, String id);

List<Slide> findAllByAttachmentUnitId(Long attachmentUnitId);

/**
* Find all slides that have a non-null hidden timestamp
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ public AttachmentUnit createAttachmentUnit(AttachmentUnit attachmentUnit, Attach
* @param studentVersionFile The student version of the original file.
* @param keepFilename Whether to keep the original filename or not.
* @param hiddenPages The hidden pages of attachment unit.
* @param pageOrder The new order of the edited attachment unit
* @return The updated attachment unit.
*/
public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit, AttachmentUnit updateUnit, Attachment updateAttachment, MultipartFile updateFile,
MultipartFile studentVersionFile, boolean keepFilename, String hiddenPages) {
MultipartFile studentVersionFile, boolean keepFilename, String hiddenPages, String pageOrder) {
Set<CompetencyLectureUnitLink> existingCompetencyLinks = new HashSet<>(existingAttachmentUnit.getCompetencyLinks());

existingAttachmentUnit.setDescription(updateUnit.getDescription());
Expand Down Expand Up @@ -136,7 +137,7 @@ public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit
if (updateFile != null) {
// Split the updated file into single slides only if it is a pdf
if (Objects.equals(FilenameUtils.getExtension(updateFile.getOriginalFilename()), "pdf")) {
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit, hiddenPages);
slideSplitterService.splitAttachmentUnitIntoSingleSlides(savedAttachmentUnit, hiddenPages, pageOrder);
}
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 @@ -10,12 +10,12 @@
import java.nio.file.Path;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.imageio.ImageIO;
Expand Down Expand Up @@ -75,22 +75,23 @@ public SlideSplitterService(FileService fileService, SlideRepository slideReposi
*/
@Async
public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit) {
splitAttachmentUnitIntoSingleSlides(attachmentUnit, null);
splitAttachmentUnitIntoSingleSlides(attachmentUnit, null, null);
}

/**
* 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.
* @param pageOrder The page order of the attachment unit.
*/
@Async
public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit, String hiddenPages) {
public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit, String hiddenPages, String pageOrder) {
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, hiddenPages);
splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename, hiddenPages, pageOrder);
}
catch (IOException e) {
log.error("Error while splitting Attachment Unit {} into single slides", attachmentUnit.getId(), e);
Expand All @@ -106,7 +107,7 @@ public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit, S
* @param pdfFilename The name of the PDF file.
*/
public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentUnit attachmentUnit, String pdfFilename) {
splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename, null);
splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename, null, null);
}

/**
Expand All @@ -115,49 +116,58 @@ public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentU
* @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.
* @param hiddenPages The hidden pages information.
* @param pageOrder The order of pages in the PDF.
*/
public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentUnit attachmentUnit, String pdfFilename, String hiddenPages) {
log.debug("Splitting Attachment Unit file {} into single slides", attachmentUnit.getAttachment().getName());
public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentUnit attachmentUnit, String pdfFilename, String hiddenPages, String pageOrder) {
log.debug("Processing slides for Attachment Unit {}", attachmentUnit.getAttachment().getName());
try {
String fileNameWithOutExt = FilenameUtils.removeExtension(pdfFilename);
int numPages = document.getNumberOfPages();
PDFRenderer pdfRenderer = new PDFRenderer(document);
List<Map<String, Object>> pageOrderList = new ObjectMapper().readValue(pageOrder, new TypeReference<>() {
});

Map<Integer, Map<String, Object>> hiddenPagesData = Collections.emptyMap();
if (hiddenPages != null) {
List<Map<String, Object>> hiddenPagesList = new ObjectMapper().readValue(hiddenPages, new TypeReference<List<Map<String, Object>>>() {
Map<String, Map<String, Object>> hiddenPagesMap = new HashMap<>();
if (hiddenPages != null && !hiddenPages.isEmpty()) {
List<Map<String, Object>> hiddenPagesList = new ObjectMapper().readValue(hiddenPages, new TypeReference<>() {
});
hiddenPagesData = hiddenPagesList.stream().collect(Collectors.toMap(map -> (Integer) map.get("pageIndex"), map -> {

hiddenPagesMap = hiddenPagesList.stream().collect(Collectors.toMap(page -> (String) page.get("slideId"), page -> {
Map<String, Object> data = new HashMap<>();
String dateStr = (String) map.get("date");
String dateStr = (String) page.get("date");
data.put("date", Timestamp.from(Instant.parse(dateStr)));

// Include exercise ID if it exists
if (map.get("exerciseId") != null) {
data.put("exerciseId", map.get("exerciseId"));
if (page.get("exerciseId") != null) {
data.put("exerciseId", page.get("exerciseId"));
}
return data;
}));
}

for (int page = 0; page < numPages; page++) {
BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(page, 72, ImageType.RGB);
byte[] imageInByte = bufferedImageToByteArray(bufferedImage, "png");
int slideNumber = page + 1;
String filename = fileNameWithOutExt + "_" + attachmentUnit.getId() + "_Slide_" + slideNumber + ".png";
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));

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);

// Get the hidden data for this slide
Map<String, Object> hiddenData = hiddenPagesData.get(slideNumber);
List<Slide> existingSlides = slideRepository.findAllByAttachmentUnitId(attachmentUnit.getId());
Map<String, Slide> existingSlidesMap = existingSlides.stream().collect(Collectors.toMap(Slide::getId, slide -> slide));

PDFRenderer pdfRenderer = new PDFRenderer(document);
String fileNameWithOutExt = FilenameUtils.removeExtension(pdfFilename);

for (Map<String, Object> page : pageOrderList) {
String slideId = (String) page.get("slideId");
int order = ((Number) page.get("order")).intValue();
int pageIndex = ((Number) page.get("pageIndex")).intValue();

Slide slide;
boolean isNewSlide = false;

if (slideId.startsWith("temp_") || !existingSlidesMap.containsKey(slideId)) {
isNewSlide = true;
slide = new Slide();
slide.setAttachmentUnit(attachmentUnit);
}
else {
slide = existingSlidesMap.get(slideId);
}

slide.setSlideNumber(order);

Map<String, Object> hiddenData = hiddenPagesMap.get(slideId);
java.util.Date previousHiddenValue = slide.getHidden();

if (hiddenData != null && hiddenData.containsKey("date")) {
Expand All @@ -177,6 +187,20 @@ public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentU
slide.setExercise(null);
}

if (isNewSlide) {
int pdfPageIndex = pageIndex - 1;
if (pdfPageIndex >= 0 && pdfPageIndex < document.getNumberOfPages()) {
BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(pdfPageIndex, 72, ImageType.RGB);
byte[] imageInByte = bufferedImageToByteArray(bufferedImage, "png");
String filename = fileNameWithOutExt + "_" + attachmentUnit.getId() + "_Slide_" + order + ".png";
MultipartFile slideFile = fileService.convertByteArrayToMultipart(filename, ".png", imageInByte);
Path savePath = fileService.saveFile(slideFile, FilePathService.getAttachmentUnitFilePath().resolve(attachmentUnit.getId().toString()).resolve("slide")
.resolve(String.valueOf(order)).resolve(filename));

slide.setSlideImagePath(FilePathService.publicPathForActualPath(savePath, (long) order).toString());
}
}

Slide savedSlide = slideRepository.save(slide);

// Schedule unhiding if the hidden date has changed
Expand All @@ -185,10 +209,20 @@ public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentU
log.debug("Scheduled unhiding for slide ID {} at time {}", savedSlide.getId(), slide.getHidden());
}
}

// Clean up slides that are no longer in the page order
Set<String> slideIdsInPageOrder = pageOrderList.stream().map(page -> (String) page.get("slideId")).collect(Collectors.toSet());

List<Slide> slidesToRemove = existingSlides.stream().filter(slide -> !slideIdsInPageOrder.contains(slide.getId())).toList();

if (!slidesToRemove.isEmpty()) {
slideRepository.deleteAll(slidesToRemove);
log.debug("Removed {} slides that are no longer in the page order", slidesToRemove.size());
}
}
catch (IOException e) {
log.error("Error while splitting Attachment Unit {} into single slides", attachmentUnit.getId(), e);
throw new InternalServerErrorException("Could not split Attachment Unit into single slides: " + e.getMessage());
log.error("Error while processing slides for Attachment Unit {}", attachmentUnit.getId(), e);
throw new InternalServerErrorException("Could not process slides: " + e.getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public ResponseEntity<AttachmentUnit> getAttachmentUnit(@PathVariable Long attac
* @param attachment the attachment with updated content
* @param file the optional file to upload
* @param hiddenPages the pages to be hidden in the attachment unit
* @param pageOrder the new order of the edited attachment unit
* @param studentVersion the student version of the 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
Expand All @@ -126,15 +127,15 @@ public ResponseEntity<AttachmentUnit> getAttachmentUnit(@PathVariable Long attac
@EnforceAtLeastEditor
public ResponseEntity<AttachmentUnit> updateAttachmentUnit(@PathVariable Long lectureId, @PathVariable Long attachmentUnitId, @RequestPart AttachmentUnit attachmentUnit,
@RequestPart Attachment attachment, @RequestPart(required = false) MultipartFile file, @RequestPart(required = false) MultipartFile studentVersion,
@RequestPart(required = false) String hiddenPages, @RequestParam(defaultValue = "false") boolean keepFilename,
@RequestPart(required = false) String hiddenPages, @RequestPart(required = false) String pageOrder, @RequestParam(defaultValue = "false") boolean keepFilename,
@RequestParam(value = "notificationText", required = false) String notificationText) {
log.debug("REST request to update an attachment unit : {}", attachmentUnit);
AttachmentUnit existingAttachmentUnit = attachmentUnitRepository.findWithSlidesAndCompetenciesByIdElseThrow(attachmentUnitId);
checkAttachmentUnitCourseAndLecture(existingAttachmentUnit, lectureId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, existingAttachmentUnit.getLecture().getCourse(), null);

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

if (notificationText != null) {
groupNotificationService.notifyStudentGroupAboutAttachmentChange(savedAttachmentUnit.getAttachment(), notificationText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<div class="popover-title mb-2">
<h5>
@if (isMultiplePages()) {
<span jhiTranslate="artemisApp.attachment.pdfPreview.dateBox.hideMultiplePages" [translateValues]="{ param: pageIndicesSorted() }"></span>
<span jhiTranslate="artemisApp.attachment.pdfPreview.dateBox.hideMultiplePages" [translateValues]="{ param: pagesDisplay() }"></span>
} @else {
<span jhiTranslate="artemisApp.attachment.pdfPreview.dateBox.hidePage" [translateValues]="{ param: pageIndices()! }"></span>
<span jhiTranslate="artemisApp.attachment.pdfPreview.dateBox.hidePage" [translateValues]="{ param: pagesDisplay() }"></span>
}
</h5>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Course } from 'app/entities/course.model';
import { Exercise, ExerciseType } from 'app/entities/exercise.model';
import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service';
import dayjs from 'dayjs/esm';
import { HiddenPage } from 'app/lecture/pdf-preview/pdf-preview.component';
import { HiddenPage, OrderedPage } from 'app/lecture/pdf-preview/pdf-preview.component';
import { AlertService } from 'app/core/util/alert.service';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
Expand All @@ -29,7 +29,7 @@ const FOREVER = dayjs('9999-12-31');
export class PdfPreviewDateBoxComponent implements OnInit {
// Inputs
course = input<Course>();
pageIndices = input<number[]>([]);
selectedPages = input<OrderedPage[]>([]);

// Signals
calendarSelected = signal<boolean>(false);
Expand All @@ -39,25 +39,32 @@ export class PdfPreviewDateBoxComponent implements OnInit {
categorizedExercises = signal<CategorizedExercise[]>([]);
hideForever = signal<boolean>(false);
selectedExercise = signal<Exercise | null>(null);
isMultiplePages = signal<boolean>(false);

// Outputs
hiddenPagesOutput = output<HiddenPage[]>();
selectionCancelledOutput = output<boolean>();

// Computed properties
pageIndicesSorted = computed(() => {
const indices = [...this.pageIndices()];
return indices.sort((a, b) => a - b).join(', ');
pagesDisplay = computed(() => {
const pages = this.selectedPages();

if (pages.length === 1) {
return `${pages[0].pageIndex}`;
}

return pages
.map((p) => p.pageIndex)
.sort()
.join(', ');
});
isMultiplePages = computed(() => this.selectedPages().length > 1);

// Injected services
private readonly alertService = inject(AlertService);
private readonly courseExerciseService = inject(CourseExerciseService);

ngOnInit(): void {
this.loadExercises();
this.isMultiplePages.set(this.pageIndices().length > 1);
}

/**
Expand Down Expand Up @@ -169,8 +176,8 @@ export class PdfPreviewDateBoxComponent implements OnInit {
return;
}

const hiddenPages: HiddenPage[] = this.pageIndices().map((pageIndex) => ({
pageIndex,
const hiddenPages: HiddenPage[] = this.selectedPages().map((page) => ({
slideId: page.slideId,
date: selectedDate,
exerciseId: this.selectedExercise()?.id ?? null,
}));
Expand Down
Loading
Loading