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

Communication: Fix broken link when referencing a lecture attachment in a post #10164

Merged
merged 3 commits into from
Jan 19, 2025
Merged
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
20 changes: 16 additions & 4 deletions src/main/webapp/app/shared/http/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,26 @@ export class FileService {
* @param downloadName the name given to the attachment
*/
downloadFileByAttachmentName(downloadUrl: string, downloadName: string) {
const normalizedDownloadUrl = this.createAttachmentFileUrl(downloadUrl, downloadName, true);
const newWindow = window.open('about:blank');
newWindow!.location.href = normalizedDownloadUrl;
return newWindow;
}

/**
* Creates the URL to download a attachment file
*
* @param downloadUrl url that is stored in the attachment model
* @param downloadName the name given to the attachment
* @param encodeName whether or not to encode the downloadName
*/
createAttachmentFileUrl(downloadUrl: string, downloadName: string, encodeName: boolean) {
const downloadUrlComponents = downloadUrl.split('/');
// take the last element
const extension = downloadUrlComponents.pop()!.split('.').pop();
const restOfUrl = downloadUrlComponents.join('/');
const normalizedDownloadUrl = restOfUrl + '/' + encodeURIComponent(downloadName + '.' + extension);
const newWindow = window.open('about:blank');
newWindow!.location.href = normalizedDownloadUrl;
return newWindow;
const encodedDownloadName = encodeName ? encodeURIComponent(downloadName + '.' + extension) : downloadName + '.' + extension;
return restOfUrl + '/' + encodedDownloadName;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { OrderedListAction } from 'app/shared/monaco-editor/model/actions/ordere
import { StrikethroughAction } from 'app/shared/monaco-editor/model/actions/strikethrough.action';
import { PostingContentComponent } from '../posting-content/posting-content.components';
import { NgStyle } from '@angular/common';
import { FileService } from 'app/shared/http/file.service';

@Component({
selector: 'jhi-posting-markdown-editor',
Expand All @@ -64,6 +65,7 @@ import { NgStyle } from '@angular/common';
export class PostingMarkdownEditorComponent implements OnInit, ControlValueAccessor, AfterContentChecked, AfterViewInit {
private cdref = inject(ChangeDetectorRef);
private metisService = inject(MetisService);
private fileService = inject(FileService);
private courseManagementService = inject(CourseManagementService);
private lectureService = inject(LectureService);
private channelService = inject(ChannelService);
Expand Down Expand Up @@ -119,7 +121,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces
...faqAction,
];

this.lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(this.metisService, this.lectureService);
this.lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(this.metisService, this.lectureService, this.fileService);
}

ngAfterViewInit(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Slide } from 'app/entities/lecture-unit/slide.model';
import { LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model';
import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface';
import { sanitizeStringForMarkdownEditor } from 'app/shared/util/markdown.util';
import { FileService } from 'app/shared/http/file.service';
import { cloneDeep } from 'lodash-es';

interface LectureWithDetails {
id: number;
Expand Down Expand Up @@ -37,19 +39,30 @@ export class LectureAttachmentReferenceAction extends TextEditorAction {
constructor(
private readonly metisService: MetisService,
private readonly lectureService: LectureService,
private readonly fileService: FileService,
) {
super(LectureAttachmentReferenceAction.ID, 'artemisApp.metis.editor.lecture');
firstValueFrom(this.lectureService.findAllByCourseIdWithSlides(this.metisService.getCourse().id!)).then((response) => {
const lectures = response.body;
if (lectures) {
this.lecturesWithDetails = lectures
.filter((lecture) => !!lecture.id && !!lecture.title)
.map((lecture) => ({
id: lecture.id!,
title: lecture.title!,
attachmentUnits: lecture.lectureUnits?.filter((unit) => unit.type === LectureUnitType.ATTACHMENT),
attachments: lecture.attachments,
}));
.map((lecture) => {
const attachmentsWithFileUrls = cloneDeep(lecture.attachments)?.map((attachment) => {
if (attachment.link && attachment.name) {
attachment.link = this.fileService.createAttachmentFileUrl(attachment.link!, attachment.name!, false);
}

return attachment;
});

return {
id: lecture.id!,
title: lecture.title!,
attachmentUnits: lecture.lectureUnits?.filter((unit) => unit.type === LectureUnitType.ATTACHMENT),
attachments: attachmentsWithFileUrls,
};
});
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ import { TextEditorRange } from 'app/shared/monaco-editor/model/actions/adapter/
import { TextEditorPosition } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-position.model';
import { BulletedListAction } from 'app/shared/monaco-editor/model/actions/bulleted-list.action';
import { OrderedListAction } from 'app/shared/monaco-editor/model/actions/ordered-list.action';
import { ListAction } from '../../../../../../../main/webapp/app/shared/monaco-editor/model/actions/list.action';
import { ListAction } from 'app/shared/monaco-editor/model/actions/list.action';
import monaco from 'monaco-editor';
import { FileService } from 'app/shared/http/file.service';
import { MockFileService } from '../../../../helpers/mocks/service/mock-file.service';

describe('PostingsMarkdownEditor', () => {
let component: PostingMarkdownEditorComponent;
let fixture: ComponentFixture<PostingMarkdownEditorComponent>;
let debugElement: DebugElement;
let mockMarkdownEditorComponent: MarkdownEditorMonacoComponent;
let metisService: MetisService;
let fileService: FileService;
let lectureService: LectureService;
let findLectureWithDetailsSpy: jest.SpyInstance;

Expand Down Expand Up @@ -120,6 +123,7 @@ describe('PostingsMarkdownEditor', () => {
return TestBed.configureTestingModule({
providers: [
{ provide: MetisService, useClass: MockMetisService },
{ provide: FileService, useClass: MockFileService },
MockProvider(LectureService),
MockProvider(CourseManagementService),
MockProvider(ChannelService),
Expand All @@ -134,6 +138,7 @@ describe('PostingsMarkdownEditor', () => {
fixture = TestBed.createComponent(PostingMarkdownEditorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fileService = TestBed.inject(FileService);
metisService = TestBed.inject(MetisService);
lectureService = TestBed.inject(LectureService);

Expand All @@ -154,14 +159,14 @@ describe('PostingsMarkdownEditor', () => {
containDefaultActions(component.defaultActions);
expect(component.defaultActions).toEqual(expect.arrayContaining([expect.any(UserMentionAction), expect.any(ChannelReferenceAction)]));

expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService, fileService));
});

it('should have set the correct default commands on init if communication is disabled', () => {
jest.spyOn(CourseModel, 'isCommunicationEnabled').mockReturnValueOnce(false);
component.ngOnInit();
containDefaultActions(component.defaultActions);
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService, fileService));
});

function containDefaultActions(defaultActions: TextEditorAction[]) {
Expand All @@ -186,15 +191,15 @@ describe('PostingsMarkdownEditor', () => {
component.ngOnInit();
containDefaultActions(component.defaultActions);
expect(component.defaultActions).toEqual(expect.arrayContaining([expect.any(FaqReferenceAction)]));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService, fileService));
});

it('should have set the correct default commands on init if faq is disabled', () => {
jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(false);
component.ngOnInit();
containDefaultActions(component.defaultActions);
expect(component.defaultActions).toEqual(expect.not.arrayContaining([expect.any(FaqReferenceAction)]));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService));
expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService, fileService));
});

it('should show the correct amount of characters below the markdown input', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import { Attachment } from 'app/entities/attachment.model';
import dayjs from 'dayjs/esm';
import { FaqReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/faq-reference.action';
import { Faq } from 'app/entities/faq.model';
import { FileService } from 'app/shared/http/file.service';
import { MockFileService } from '../../../helpers/mocks/service/mock-file.service';

describe('MonacoEditorCommunicationActionIntegration', () => {
let comp: MonacoEditorComponent;
let fixture: ComponentFixture<MonacoEditorComponent>;
let metisService: MetisService;
let fileService: FileService;
let courseManagementService: CourseManagementService;
let channelService: ChannelService;
let lectureService: LectureService;
Expand All @@ -46,34 +49,34 @@ describe('MonacoEditorCommunicationActionIntegration', () => {
let exerciseReferenceAction: ExerciseReferenceAction;
let faqReferenceAction: FaqReferenceAction;

beforeEach(() => {
return TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MonacoEditorComponent],
providers: [
{ provide: MetisService, useClass: MockMetisService },
{ provide: FileService, useClass: MockFileService },
{ provide: TranslateService, useClass: MockTranslateService },
{ provide: LocalStorageService, useClass: MockLocalStorageService },
MockProvider(LectureService),
MockProvider(CourseManagementService),
MockProvider(ChannelService),
],
})
.compileComponents()
.then(() => {
global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => {
return new MockResizeObserver(callback);
});
fixture = TestBed.createComponent(MonacoEditorComponent);
comp = fixture.componentInstance;
metisService = TestBed.inject(MetisService);
courseManagementService = TestBed.inject(CourseManagementService);
lectureService = TestBed.inject(LectureService);
channelService = TestBed.inject(ChannelService);
channelReferenceAction = new ChannelReferenceAction(metisService, channelService);
userMentionAction = new UserMentionAction(courseManagementService, metisService);
exerciseReferenceAction = new ExerciseReferenceAction(metisService);
faqReferenceAction = new FaqReferenceAction(metisService);
});
}).compileComponents();

global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => {
return new MockResizeObserver(callback);
});
fixture = TestBed.createComponent(MonacoEditorComponent);
comp = fixture.componentInstance;
metisService = TestBed.inject(MetisService);
fileService = TestBed.inject(FileService);
courseManagementService = TestBed.inject(CourseManagementService);
lectureService = TestBed.inject(LectureService);
channelService = TestBed.inject(ChannelService);
channelReferenceAction = new ChannelReferenceAction(metisService, channelService);
userMentionAction = new UserMentionAction(courseManagementService, metisService);
exerciseReferenceAction = new ExerciseReferenceAction(metisService);
faqReferenceAction = new FaqReferenceAction(metisService);
});

afterEach(() => {
Expand Down Expand Up @@ -280,7 +283,7 @@ describe('MonacoEditorCommunicationActionIntegration', () => {
beforeEach(() => {
lectures = metisService.getCourse().lectures!;
jest.spyOn(lectureService, 'findAllByCourseIdWithSlides').mockReturnValue(of(new HttpResponse({ body: lectures, status: 200 })));
lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(metisService, lectureService);
lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(metisService, lectureService, fileService);
});

afterEach(() => {
Expand All @@ -295,7 +298,10 @@ describe('MonacoEditorCommunicationActionIntegration', () => {
id: lecture.id!,
title: lecture.title!,
attachmentUnits: lecture.lectureUnits?.filter((unit) => unit.type === LectureUnitType.ATTACHMENT),
attachments: lecture.attachments,
attachments: lecture.attachments?.map((attachment) => ({
...attachment,
link: attachment.link && attachment.name ? fileService.createAttachmentFileUrl(attachment.link, attachment.name, false) : attachment.link,
})),
}));

expect(lectureAttachmentReferenceAction.lecturesWithDetails).toEqual(lecturesWithDetails);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class MockFileService {
return of();
};

createAttachmentFileUrl(downloadUrl: string, downloadName: string, encodeName: boolean) {
return 'attachments/' + downloadName.replace(' ', '-') + '.pdf';
}

replaceLectureAttachmentPrefixAndUnderscores = (link: string) => link;
replaceAttachmentPrefixAndUnderscores = (link: string) => link;
}
Loading