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

Development: Reimplement course icon file upload #5733

Merged
merged 21 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
205 changes: 102 additions & 103 deletions src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java

Large diffs are not rendered by default.

33 changes: 26 additions & 7 deletions src/main/webapp/app/course/manage/course-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CourseManagementDetailViewDto } from 'app/course/manage/course-manageme
import { StudentDTO } from 'app/entities/student-dto.model';
import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service';
import { convertDateFromClient } from 'app/utils/date.utils';
import { objectToJsonBlob } from 'app/utils/blob-util';

export type EntityResponseType = HttpResponse<Course>;
export type EntityArrayResponseType = HttpResponse<Course[]>;
Expand All @@ -37,19 +38,37 @@ export class CourseManagementService {
/**
* creates a course using a POST request
* @param course - the course to be created on the server
* @param courseImage - the course icon file
*/
create(course: Course): Observable<EntityResponseType> {
create(course: Course, courseImage?: Blob): Observable<EntityResponseType> {
const copy = CourseManagementService.convertCourseDatesFromClient(course);
return this.http.post<Course>(this.resourceUrl, copy, { observe: 'response' }).pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res)));
const formData = new FormData();
formData.append('course', objectToJsonBlob(copy));
if (courseImage) {
// The image was cropped by us and is a blob, so we need to set a placeholder name for the server check
formData.append('file', courseImage, 'placeholderName.png');
}

return this.http.post<Course>(this.resourceUrl, formData, { observe: 'response' }).pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res)));
}

/**
* updates a course using a PUT request
* @param course - the course to be updated
*/
update(course: Course): Observable<EntityResponseType> {
const copy = CourseManagementService.convertCourseDatesFromClient(course);
return this.http.put<Course>(this.resourceUrl, copy, { observe: 'response' }).pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res)));
* @param courseId - the id of the course to be updated
* @param courseUpdate - the updates to the course
* @param courseImage - the course icon file
*/
update(courseId: number, courseUpdate: Course, courseImage?: Blob): Observable<EntityResponseType> {
const copy = CourseManagementService.convertCourseDatesFromClient(courseUpdate);
const formData = new FormData();
formData.append('course', objectToJsonBlob(copy));
if (courseImage) {
// The image was cropped by us and is a blob, so we need to set a placeholder name for the server check
formData.append('file', courseImage, 'placeholderName.png');
}
return this.http
.put<Course>(`${this.resourceUrl}/${courseId}`, formData, { observe: 'response' })
.pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ <h4 *ngIf="course.id" id="jhi-course-heading-edit" jhiTranslate="artemisApp.cour
<div class="row">
<div class="col-6">
<image-cropper
[imageChangedEvent]="imageChangedEvent"
[imageFile]="courseImageUploadFile"
[maintainAspectRatio]="true"
[aspectRatio]="1"
[resizeToWidth]="200"
Expand All @@ -67,12 +67,6 @@ <h4 *ngIf="course.id" id="jhi-course-heading-edit" jhiTranslate="artemisApp.cour
[style.display]="showCropper ? null : 'none'"
></image-cropper>
</div>
<div *ngIf="showCropper" class="col-3">
<button class="btn btn-outline-primary icon-upload" (click)="uploadCourseImage()" [disabled]="isUploadingCourseImage || !courseImageFileName">
<span *ngIf="isUploadingCourseImage" jhiTranslate="artemisApp.course.uploading"></span>
<span *ngIf="!isUploadingCourseImage" jhiTranslate="artemisApp.course.upload"></span>
</button>
</div>
</div>
</div>
<div class="form-group">
Expand Down
56 changes: 14 additions & 42 deletions src/main/webapp/app/course/manage/course-update.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,8 @@ export class CourseUpdateComponent implements OnInit {
onlineCourseConfigurationForm: FormGroup;
course: Course;
isSaving: boolean;
courseImageFile?: File;
courseImageFileName: string;
isUploadingCourseImage: boolean;
imageChangedEvent: any = '';
croppedImage: any = '';
courseImageUploadFile?: File;
croppedImage?: string;
showCropper = false;
presentationScoreEnabled = false;
complaintsEnabled = true; // default value
Expand Down Expand Up @@ -193,8 +190,7 @@ export class CourseUpdateComponent implements OnInit {
},
{ validators: CourseValidator },
);
this.courseImageFileName = this.course.courseIcon!;
this.croppedImage = this.course.courseIcon ? this.course.courseIcon : '';
this.croppedImage = this.course.courseIcon;
this.presentationScoreEnabled = this.course.presentationScore !== 0;
}

Expand All @@ -220,10 +216,15 @@ export class CourseUpdateComponent implements OnInit {
const course = this.courseForm.getRawValue();
course.onlineCourseConfiguration = this.isOnlineCourse() ? this.onlineCourseConfigurationForm.getRawValue() : null;

let file = undefined;
if (this.courseImageUploadFile && this.croppedImage) {
const base64Data = this.croppedImage.replace('data:image/png;base64,', '');
file = base64StringToBlob(base64Data, 'image/*');
}
if (this.course.id !== undefined) {
this.subscribeToSaveResponse(this.courseService.update(course));
this.subscribeToSaveResponse(this.courseService.update(this.course.id, course, file));
} else {
this.subscribeToSaveResponse(this.courseService.create(course));
this.subscribeToSaveResponse(this.courseService.create(course, file));
}
}

Expand Down Expand Up @@ -258,12 +259,10 @@ export class CourseUpdateComponent implements OnInit {
* @function set course icon
* @param event {object} Event object which contains the uploaded file
*/
setCourseImage(event: any): void {
this.imageChangedEvent = event;
if (event.target.files.length) {
const fileList: FileList = event.target.files;
this.courseImageFile = fileList[0];
this.courseImageFileName = this.courseImageFile.name;
setCourseImage(event: Event): void {
const element = event.currentTarget as HTMLInputElement;
if (element.files?.[0]) {
this.courseImageUploadFile = element.files[0];
}
}

Expand All @@ -278,33 +277,6 @@ export class CourseUpdateComponent implements OnInit {
this.showCropper = true;
}

/**
* @function uploadBackground
* @desc Upload the selected file (from "Upload Background") and use it for the question's backgroundFilePath
*/
uploadCourseImage(): void {
const contentType = 'image/*';
const base64Data = this.croppedImage.replace('data:image/png;base64,', '');
const file = base64StringToBlob(base64Data, contentType);
const fileName = this.courseImageFileName;

this.isUploadingCourseImage = true;
this.fileUploaderService.uploadFile(file, fileName).then(
(response) => {
this.courseForm.patchValue({ courseIcon: response.path });
this.isUploadingCourseImage = false;
this.courseImageFile = undefined;
this.courseImageFileName = response.path!;
},
() => {
this.isUploadingCourseImage = false;
this.courseImageFile = undefined;
this.courseImageFileName = this.course.courseIcon!;
},
);
this.showCropper = false;
}

/**
* Action on unsuccessful course creation or edit
* @param error The error for providing feedback
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/app/shared/http/file-uploader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class FileUploaderService {
return Promise.reject(new Error('File is too big! Maximum allowed file size: ' + MAX_FILE_SIZE / (1024 * 1024) + ' MB.'));
}

const keepFileName: boolean = !!options && options.keepFileName;
const keepFileName: boolean = !!options?.keepFileName;
const formData = new FormData();
formData.append('file', file, fileName);
return lastValueFrom(this.http.post<FileUploadResponse>(endpoint + `?keepFileName=${keepFileName}`, formData));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CropperPosition } from 'app/shared/image-cropper/interfaces/cropper-pos
import { ImageCroppedEvent } from 'app/shared/image-cropper/interfaces/image-cropped-event.interface';

// Note: this component and all files in the image-cropper folder were taken from https://github.com/Mawi137/ngx-image-cropper because the framework was not maintained anymore
// Note: Partially adapted to fit Artemis needs

@Component({
// tslint:disable-next-line:component-selector
Expand All @@ -37,10 +38,10 @@ import { ImageCroppedEvent } from 'app/shared/image-cropper/interfaces/image-cro
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImageCropperComponent implements OnChanges, OnInit {
private settings = new CropperSettings();
private setImageMaxSizeRetries = 0;
private moveStart: MoveStart;
private loadedImage?: LoadedImage;
settings = new CropperSettings();
setImageMaxSizeRetries = 0;
moveStart: MoveStart;
loadedImage?: LoadedImage;

safeImgDataUrl: SafeUrl | string;
safeTransformStyle: SafeStyle | string;
Expand All @@ -52,10 +53,10 @@ export class ImageCropperComponent implements OnChanges, OnInit {
@ViewChild('wrapper', { static: true }) wrapper: ElementRef<HTMLDivElement>;
@ViewChild('sourceImage', { static: false }) sourceImage: ElementRef<HTMLDivElement>;

@Input() imageChangedEvent: any;
@Input() imageURL: string;
@Input() imageBase64: string;
@Input() imageFile: File;
@Input() imageChangedEvent?: Event;
@Input() imageURL?: string;
@Input() imageBase64?: string;
@Input() imageFile?: File;
julian-christl marked this conversation as resolved.
Show resolved Hide resolved

@Input() format: OutputFormat = this.settings.format;
@Input() transform: ImageTransform = {};
Expand Down Expand Up @@ -107,6 +108,10 @@ export class ImageCropperComponent implements OnChanges, OnInit {
this.reset();
}

ngOnInit(): void {
this.settings.stepSize = this.initialStepSize;
}

ngOnChanges(changes: SimpleChanges): void {
this.onChangesUpdateSettings(changes);
this.onChangesInputImage(changes);
Expand Down Expand Up @@ -156,7 +161,10 @@ export class ImageCropperComponent implements OnChanges, OnInit {
this.reset();
}
if (changes.imageChangedEvent && this.isValidImageChangedEvent()) {
this.loadImageFile(this.imageChangedEvent.target.files[0]);
const element = this.imageChangedEvent?.currentTarget as HTMLInputElement;
if (element.files) {
this.loadImageFile(element.files[0]);
}
}
if (changes.imageURL && this.imageURL) {
this.loadImageFromURL(this.imageURL);
Expand All @@ -170,7 +178,12 @@ export class ImageCropperComponent implements OnChanges, OnInit {
}

private isValidImageChangedEvent(): boolean {
return this.imageChangedEvent?.target?.files?.length > 0;
if (!this.imageChangedEvent?.currentTarget) {
return false;
}
const element = this.imageChangedEvent.currentTarget as HTMLInputElement;

return !!element.files?.length;
}

private setCssTransform() {
Expand All @@ -187,10 +200,6 @@ export class ImageCropperComponent implements OnChanges, OnInit {
);
}

ngOnInit(): void {
this.settings.stepSize = this.initialStepSize;
}

private reset(): void {
this.imageVisible = false;
this.loadedImage = undefined;
Expand Down
8 changes: 4 additions & 4 deletions src/test/cypress/integration/CourseManagement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Interception } from 'cypress/types/net-stubbing';
import { COURSE_BASE } from '../support/requests/CourseManagementRequests';
import { COURSE_BASE, convertCourseAfterMultiPart } from '../support/requests/CourseManagementRequests';
import { BASE_API, GET, POST } from '../support/constants';
import { artemis } from '../support/ArtemisTesting';
import { CourseManagementPage } from '../support/pageobjects/course/CourseManagementPage';
Expand Down Expand Up @@ -40,9 +40,9 @@ describe('Course management', () => {
let courseId: number;

beforeEach(() => {
artemisRequests.courseManagement.createCourse(false, courseName, courseShortName).then((response: Cypress.Response<Course>) => {
course = response.body;
courseId = response.body!.id!;
artemisRequests.courseManagement.createCourse(false, courseName, courseShortName).then((response) => {
course = convertCourseAfterMultiPart(response);
courseId = course.id!;
});
});

Expand Down
3 changes: 2 additions & 1 deletion src/test/cypress/integration/Logout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { artemis } from '../support/ArtemisTesting';
import { authTokenKey } from '../support/constants';
import { Course } from '../../../main/webapp/app/entities/course.model';
import { ModelingExercise } from '../../../main/webapp/app/entities/modeling-exercise.model';
import { convertCourseAfterMultiPart } from '../support/requests/CourseManagementRequests';

const courseRequests = artemis.requests.courseManagement;
const users = artemis.users;
Expand All @@ -18,7 +19,7 @@ describe('Logout tests', () => {
cy.login(admin);

courseRequests.createCourse(true).then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
courseRequests.createModelingExercise({ course }).then((resp: Cypress.Response<ModelingExercise>) => {
modelingExercise = resp.body;
});
Expand Down
4 changes: 2 additions & 2 deletions src/test/cypress/integration/exam/ExamAssessment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Course } from 'app/entities/course.model';
import { ExerciseGroup } from 'app/entities/exercise-group.model';
import { Exam } from 'app/entities/exam.model';
import { artemis } from '../../support/ArtemisTesting';
import { CypressAssessmentType, CypressExamBuilder } from '../../support/requests/CourseManagementRequests';
import { CypressAssessmentType, CypressExamBuilder, convertCourseAfterMultiPart } from '../../support/requests/CourseManagementRequests';
import partiallySuccessful from '../../fixtures/programming_exercise_submissions/partially_successful/submission.json';
import dayjs, { Dayjs } from 'dayjs/esm';
import textSubmission from '../../fixtures/text_exercise_submission/text_exercise_submission.json';
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('Exam assessment', () => {
before('Create a course', () => {
cy.login(admin);
courseManagementRequests.createCourse(true).then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
courseManagementRequests.addStudentToCourse(course, artemis.users.getStudentOne());
courseManagementRequests.addTutorToCourse(course, artemis.users.getTutor());
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Interception } from 'cypress/types/net-stubbing';
import { Course } from 'app/entities/course.model';
import { CypressExamBuilder } from '../../support/requests/CourseManagementRequests';
import { CypressExamBuilder, convertCourseAfterMultiPart } from '../../support/requests/CourseManagementRequests';
import dayjs from 'dayjs/esm';
import { artemis } from '../../support/ArtemisTesting';
import { generateUUID } from '../../support/utils';
Expand All @@ -23,7 +23,7 @@ describe('Exam creation/deletion', () => {
before(() => {
cy.login(artemis.users.getAdmin());
courseManagementRequests.createCourse().then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExerciseGroup } from 'app/entities/exercise-group.model';
import { Course } from 'app/entities/course.model';
import { Exam } from 'app/entities/exam.model';
import { CypressExamBuilder } from '../../support/requests/CourseManagementRequests';
import { CypressExamBuilder, convertCourseAfterMultiPart } from '../../support/requests/CourseManagementRequests';
import dayjs from 'dayjs/esm';
import { artemis } from '../../support/ArtemisTesting';
import { generateUUID } from '../../support/utils';
Expand All @@ -22,7 +22,7 @@ describe('Exam date verification', () => {
before(() => {
cy.login(artemis.users.getAdmin());
courseManagementRequests.createCourse().then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
courseManagementRequests.addStudentToCourse(course, artemis.users.getStudentOne());
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/test/cypress/integration/exam/ExamManagement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Interception } from 'cypress/types/net-stubbing';
import { Exam } from 'app/entities/exam.model';
import { Course } from 'app/entities/course.model';
import { CypressExamBuilder } from '../../support/requests/CourseManagementRequests';
import { CypressExamBuilder, convertCourseAfterMultiPart } from '../../support/requests/CourseManagementRequests';
import { artemis } from '../../support/ArtemisTesting';
import { generateUUID } from '../../support/utils';

Expand Down Expand Up @@ -31,7 +31,7 @@ describe('Exam management', () => {
before(() => {
cy.login(users.getAdmin());
courseManagementRequests.createCourse().then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
courseManagementRequests.addStudentToCourse(course, users.getStudentOne());
const examConfig = new CypressExamBuilder(course).title(examTitle).build();
courseManagementRequests.createExam(examConfig).then((examResponse) => {
Expand Down
4 changes: 2 additions & 2 deletions src/test/cypress/integration/exam/ExamParticipation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model';
import { Exam } from 'app/entities/exam.model';
import { BASE_API, GET } from '../../support/constants';
import { CypressExamBuilder } from '../../support/requests/CourseManagementRequests';
import { CypressExamBuilder, convertCourseAfterMultiPart } from '../../support/requests/CourseManagementRequests';
import { artemis } from '../../support/ArtemisTesting';
import dayjs from 'dayjs/esm';
import submission from '../../fixtures/programming_exercise_submissions/all_successful/submission.json';
Expand Down Expand Up @@ -37,7 +37,7 @@ describe('Exam participation', () => {
before(() => {
cy.login(users.getAdmin());
courseRequests.createCourse(true).then((response) => {
course = response.body;
course = convertCourseAfterMultiPart(response);
const examContent = new CypressExamBuilder(course)
.visibleDate(dayjs().subtract(3, 'days'))
.startDate(dayjs().subtract(2, 'days'))
Expand Down
Loading