Skip to content

Commit

Permalink
Development: Reimplement course icon file upload (#5733)
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-christl committed Nov 7, 2022
1 parent bc143e3 commit 59f418e
Show file tree
Hide file tree
Showing 36 changed files with 734 additions and 376 deletions.
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;

@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

0 comments on commit 59f418e

Please sign in to comment.