diff --git a/.gitignore b/.gitignore index 62777417c82..e32f5ca7c21 100644 --- a/.gitignore +++ b/.gitignore @@ -60,5 +60,7 @@ src/client/java/teammates/client/scripts/statistics/data/ src/client/java/teammates/client/scripts/log/ src/e2e/resources/gmail-api/ src/e2e/resources/downloads/ +filestorage-dev/* +src/test/resources/filestorage/**/* !.gitkeep diff --git a/build.gradle b/build.gradle index 1eec9f64526..6415ffec9f5 100644 --- a/build.gradle +++ b/build.gradle @@ -56,19 +56,7 @@ dependencies { implementation("com.google.appengine:appengine-api-1.0-sdk:${appengineVersion}") implementation("com.google.cloud:google-cloud-tasks:1.30.11") implementation("com.google.cloud:google-cloud-logging:2.1.2") - - // This dependency needs to be resolved individually - // because the main dependency (appengine-gcs-client) does not have its transitive dependencies up-to-date - implementation("com.google.appengine.tools:appengine-gcs-client:0.8.1") { - // Use the newer servlet library instead - exclude group: "javax.servlet", module: "servlet-api" - } - implementation("com.google.api-client:google-api-client-appengine:1.30.9") { - // Use the newer servlet library instead - exclude group: "javax.servlet", module: "servlet-api" - } - implementation("com.google.apis:google-api-services-storage:v1-rev20200430-1.30.9") - + implementation("com.google.cloud:google-cloud-storage:1.113.9") implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.guava:guava:30.1-jre") implementation(objectify) @@ -280,9 +268,8 @@ appengine { port = 8080 jvmFlags = ["-Xss2m", "-Dfile.encoding=UTF-8", // Absolute paths are not supported, the following is relative to the project directory - // These only specify the datastore/blobstore paths, but search indexes are still generated in WEB-INF/appengine-generated - "-Ddatastore.backing_store=../../appengine-generated/local_db.bin", - "-Dblobstore.backing_store=../../appengine-generated"] + // These only specify the datastore paths, but search indexes are still generated in WEB-INF/appengine-generated + "-Ddatastore.backing_store=../../appengine-generated/local_db.bin"] automaticRestart = true } deploy { diff --git a/filestorage-dev/.gitkeep b/filestorage-dev/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/client/java/teammates/client/scripts/GoogleIdMigrationBaseScript.java b/src/client/java/teammates/client/scripts/GoogleIdMigrationBaseScript.java index e1d00b10709..34acec68948 100644 --- a/src/client/java/teammates/client/scripts/GoogleIdMigrationBaseScript.java +++ b/src/client/java/teammates/client/scripts/GoogleIdMigrationBaseScript.java @@ -3,17 +3,15 @@ import java.util.List; import java.util.stream.Collectors; -import com.google.appengine.tools.cloudstorage.GcsFilename; -import com.google.appengine.tools.cloudstorage.GcsService; -import com.google.appengine.tools.cloudstorage.GcsServiceFactory; -import com.google.appengine.tools.cloudstorage.RetryParams; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; import teammates.client.util.ClientProperties; import teammates.common.datatransfer.attributes.InstructorAttributes; import teammates.common.util.Config; -import teammates.common.util.GoogleCloudStorageHelper; import teammates.storage.api.InstructorsDb; import teammates.storage.entity.Account; import teammates.storage.entity.CourseStudent; @@ -100,23 +98,14 @@ protected void migrateEntity(Account oldAccount) throws Exception { ofy().delete().type(Account.class).id(oldGoogleId).now(); if (oldStudentProfile != null) { - String pictureKey = oldStudentProfile.getPictureKey(); - if (!ClientProperties.isTargetUrlDevServer()) { - try { - GcsFilename oldGcsFilename = new GcsFilename(Config.PRODUCTION_GCS_BUCKETNAME, oldGoogleId); - GcsFilename newGcsFilename = new GcsFilename(Config.PRODUCTION_GCS_BUCKETNAME, newGoogleId); - GcsService gcsService = GcsServiceFactory.createGcsService(RetryParams.getDefaultInstance()); - gcsService.copy(oldGcsFilename, newGcsFilename); - gcsService.delete(oldGcsFilename); - pictureKey = GoogleCloudStorageHelper.createBlobKey(newGoogleId); - } catch (Exception e) { - log("Profile picture not exist or error during copy: " + e.getMessage()); - } + Storage storage = StorageOptions.newBuilder().setProjectId(Config.APP_ID).build().getService(); + Blob blob = storage.get(Config.PRODUCTION_GCS_BUCKETNAME, oldGoogleId); + blob.copyTo(Config.PRODUCTION_GCS_BUCKETNAME, newGoogleId); + blob.delete(); } oldStudentProfile.setGoogleId(newGoogleId); - oldStudentProfile.setPictureKey(pictureKey); ofy().save().entity(oldStudentProfile).now(); ofy().delete().key(oldStudentProfileKey).now(); } diff --git a/src/e2e/resources/data/InstructorCourseStudentDetailsPageE2ETest.json b/src/e2e/resources/data/InstructorCourseStudentDetailsPageE2ETest.json index ee0fc74230c..8597f267338 100644 --- a/src/e2e/resources/data/InstructorCourseStudentDetailsPageE2ETest.json +++ b/src/e2e/resources/data/InstructorCourseStudentDetailsPageE2ETest.json @@ -72,8 +72,7 @@ "institute": "TEAMMATES Test Institute 7", "nationality": "Laotian", "gender": "MALE", - "moreInfo": "This is a lot of info...", - "pictureKey": "" + "moreInfo": "This is a lot of info..." } } } diff --git a/src/e2e/resources/data/InstructorStudentRecordsPageE2ETest.json b/src/e2e/resources/data/InstructorStudentRecordsPageE2ETest.json index 89e49fbdc5a..3690dddf3e5 100644 --- a/src/e2e/resources/data/InstructorStudentRecordsPageE2ETest.json +++ b/src/e2e/resources/data/InstructorStudentRecordsPageE2ETest.json @@ -70,8 +70,7 @@ "institute": "TEAMMATES Test Institute 7", "nationality": "Singaporean", "gender": "MALE", - "moreInfo": "This is a lot of info!", - "pictureKey": "" + "moreInfo": "This is a lot of info!" } } } diff --git a/src/e2e/resources/data/StudentCourseDetailsPageE2ETest.json b/src/e2e/resources/data/StudentCourseDetailsPageE2ETest.json index 4b920209171..e4d3d6ce97c 100644 --- a/src/e2e/resources/data/StudentCourseDetailsPageE2ETest.json +++ b/src/e2e/resources/data/StudentCourseDetailsPageE2ETest.json @@ -105,8 +105,7 @@ "institute": "inst", "nationality": "American", "gender": "OTHER", - "moreInfo": "I am just a student :P", - "pictureKey": "" + "moreInfo": "I am just a student :P" }, "SCDet.charlie": { "googleId": "tm.e2e.SCDet.charlie", @@ -115,8 +114,7 @@ "institute": "inst", "nationality": "Singaporean", "gender": "OTHER", - "moreInfo": "I am also a student :P", - "pictureKey": "" + "moreInfo": "I am also a student :P" } } } diff --git a/src/e2e/resources/data/StudentProfilePageE2ETest.json b/src/e2e/resources/data/StudentProfilePageE2ETest.json index e8254f5d344..3c99c049407 100644 --- a/src/e2e/resources/data/StudentProfilePageE2ETest.json +++ b/src/e2e/resources/data/StudentProfilePageE2ETest.json @@ -39,8 +39,7 @@ "institute": "TEAMMATES Test Institute 4", "nationality": "Singaporean", "gender": "MALE", - "moreInfo": "I am just another student :P", - "pictureKey": "" + "moreInfo": "I am just another student :P" } } } diff --git a/src/lnp/java/teammates/lnp/cases/StudentProfileLNPTest.java b/src/lnp/java/teammates/lnp/cases/StudentProfileLNPTest.java index a9cd0759e68..1190be0f144 100644 --- a/src/lnp/java/teammates/lnp/cases/StudentProfileLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/StudentProfileLNPTest.java @@ -120,7 +120,6 @@ protected Map generateProfiles() { .withShortName(String.valueOf(i)) .withInstitute("TEAMMATES Test Institute 222") .withMoreInfo("I am " + i) - .withPictureKey("") .withGender(StudentProfileAttributes.Gender.MALE) .withNationality("American") .build() diff --git a/src/main/java/teammates/common/datatransfer/attributes/StudentProfileAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/StudentProfileAttributes.java index 431c660590c..6e555107b08 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/StudentProfileAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/StudentProfileAttributes.java @@ -25,7 +25,6 @@ public class StudentProfileAttributes extends EntityAttributes { public String nationality; public Gender gender; public String moreInfo; - public String pictureKey; public Instant modifiedDate; private StudentProfileAttributes(String googleId) { @@ -36,7 +35,6 @@ private StudentProfileAttributes(String googleId) { this.nationality = ""; this.gender = Gender.OTHER; this.moreInfo = ""; - this.pictureKey = ""; this.modifiedDate = Instant.now(); } @@ -59,9 +57,6 @@ public static StudentProfileAttributes valueOf(StudentProfile sp) { if (sp.getMoreInfo() != null) { studentProfileAttributes.moreInfo = sp.getMoreInfo(); } - if (sp.getPictureKey() != null) { - studentProfileAttributes.pictureKey = sp.getPictureKey(); - } if (sp.getModifiedDate() != null) { studentProfileAttributes.modifiedDate = sp.getModifiedDate(); } @@ -85,7 +80,6 @@ public StudentProfileAttributes getCopy() { studentProfileAttributes.gender = gender; studentProfileAttributes.nationality = nationality; studentProfileAttributes.moreInfo = moreInfo; - studentProfileAttributes.pictureKey = pictureKey; studentProfileAttributes.modifiedDate = modifiedDate; return studentProfileAttributes; @@ -119,10 +113,6 @@ public String getMoreInfo() { return moreInfo; } - public String getPictureKey() { - return pictureKey; - } - public Instant getModifiedDate() { return modifiedDate; } @@ -153,8 +143,6 @@ public List getInvalidityInfo() { Assumption.assertNotNull(gender); - Assumption.assertNotNull(this.pictureKey); - // No validation for modified date as it is determined by the system. // No validation for More Info. It will properly sanitized. @@ -170,7 +158,7 @@ public String toString() { public int hashCode() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(this.email).append(this.shortName).append(this.institute) - .append(this.googleId).append(this.pictureKey).append(this.gender.toString()); + .append(this.googleId).append(this.gender.toString()); return stringBuilder.toString().hashCode(); } @@ -186,7 +174,6 @@ public boolean equals(Object other) { && Objects.equals(this.shortName, otherProfile.shortName) && Objects.equals(this.institute, otherProfile.institute) && Objects.equals(this.googleId, otherProfile.googleId) - && Objects.equals(this.pictureKey, otherProfile.pictureKey) && Objects.equals(this.gender, otherProfile.gender); } else { return false; @@ -196,7 +183,7 @@ public boolean equals(Object other) { @Override public StudentProfile toEntity() { return new StudentProfile(googleId, shortName, email, institute, nationality, gender.name().toLowerCase(), - moreInfo, this.pictureKey); + moreInfo); } @Override @@ -214,7 +201,6 @@ public void update(UpdateOptions updateOptions) { updateOptions.nationalityOption.ifPresent(s -> nationality = s); updateOptions.genderOption.ifPresent(s -> gender = s); updateOptions.moreInfoOption.ifPresent(s -> moreInfo = s); - updateOptions.pictureKeyOption.ifPresent(s -> pictureKey = s); } /** @@ -280,7 +266,6 @@ public static class UpdateOptions { private UpdateOption nationalityOption = UpdateOption.empty(); private UpdateOption genderOption = UpdateOption.empty(); private UpdateOption moreInfoOption = UpdateOption.empty(); - private UpdateOption pictureKeyOption = UpdateOption.empty(); private UpdateOptions(String googleId) { Assumption.assertNotNull(googleId); @@ -380,13 +365,6 @@ public B withMoreInfo(String moreInfo) { return thisBuilder; } - public B withPictureKey(String pictureKey) { - Assumption.assertNotNull(pictureKey); - - updateOptions.pictureKeyOption = UpdateOption.of(pictureKey); - return thisBuilder; - } - public abstract T build(); } diff --git a/src/main/java/teammates/common/util/Config.java b/src/main/java/teammates/common/util/Config.java index 90d1808fcb1..1b9fa3b2b80 100644 --- a/src/main/java/teammates/common/util/Config.java +++ b/src/main/java/teammates/common/util/Config.java @@ -15,9 +15,6 @@ */ public final class Config { - /** The value of the application URL, or null if no server instance is running. */ - public static final String APP_URL; - /** The value of the "app.id" in build.properties file. */ public static final String APP_ID; @@ -88,7 +85,6 @@ public final class Config { public static final boolean MAINTENANCE; static { - APP_URL = readAppUrl(); Properties properties = new Properties(); try (InputStream buildPropStream = FileHelper.getResourceAsStream("build.properties")) { properties.load(buildPropStream); @@ -124,7 +120,7 @@ private Config() { // access static fields directly } - private static String readAppUrl() { + static String getBaseAppUrl() { ApiProxy.Environment serverEnvironment = ApiProxy.getCurrentEnvironment(); if (serverEnvironment == null) { return null; @@ -179,7 +175,7 @@ public static AppUrl getFrontEndAppUrl(String relativeUrl) { * {@code relativeUrl} must start with a "/". */ private static AppUrl getBackEndAppUrl(String relativeUrl) { - return new AppUrl(APP_URL + relativeUrl); + return new AppUrl(getBaseAppUrl() + relativeUrl); } public static boolean isUsingSendgrid() { diff --git a/src/main/java/teammates/common/util/GoogleCloudStorageHelper.java b/src/main/java/teammates/common/util/GoogleCloudStorageHelper.java deleted file mode 100644 index a5c82f23dc0..00000000000 --- a/src/main/java/teammates/common/util/GoogleCloudStorageHelper.java +++ /dev/null @@ -1,88 +0,0 @@ -package teammates.common.util; - -import java.io.IOException; -import java.nio.ByteBuffer; - -import javax.servlet.http.HttpServletResponse; - -import com.google.appengine.api.blobstore.BlobKey; -import com.google.appengine.api.blobstore.BlobstoreService; -import com.google.appengine.api.blobstore.BlobstoreServiceFactory; -import com.google.appengine.tools.cloudstorage.GcsFileOptions; -import com.google.appengine.tools.cloudstorage.GcsFilename; -import com.google.appengine.tools.cloudstorage.GcsOutputChannel; -import com.google.appengine.tools.cloudstorage.GcsServiceFactory; -import com.google.appengine.tools.cloudstorage.RetryParams; - -/** - * Holds functions for operations related to Google Cloud Storage. - */ -public final class GoogleCloudStorageHelper { - - private static final Logger log = Logger.getLogger(); - - private GoogleCloudStorageHelper() { - // utility class - } - - private static BlobstoreService service() { - return BlobstoreServiceFactory.getBlobstoreService(); - } - - /** - * Returns true if a file with the specified {@code fileKey} exists in the - * Google Cloud Storage. - */ - public static boolean doesFileExistInGcs(String fileKey) { - try { - service().fetchData(new BlobKey(fileKey), 0, 1); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - /** - * Deletes the file with the specified {@code fileKey} in the Google Cloud Storage. - */ - public static void deleteFile(String fileKey) { - try { - service().delete(new BlobKey(fileKey)); - } catch (Exception e) { - log.warning("Trying to delete non-existent file with key: " + fileKey); - } - } - - /** - * Writes a byte array {@code imageData} as image to the Google Cloud Storage, - * with the {@code googleId} as the identifier name for the image. - * - * @return the {@link BlobKey} used as the image's identifier in Google Cloud Storage - */ - public static String writeImageDataToGcs(String googleId, byte[] imageData, String contentType) throws IOException { - GcsFilename gcsFilename = new GcsFilename(Config.PRODUCTION_GCS_BUCKETNAME, googleId); - try (GcsOutputChannel outputChannel = - GcsServiceFactory.createGcsService(RetryParams.getDefaultInstance()) - .createOrReplace(gcsFilename, new GcsFileOptions.Builder().mimeType(contentType).build())) { - - outputChannel.write(ByteBuffer.wrap(imageData)); - } - - return createBlobKey(googleId); - } - - /** - * Creates a blob key for the object with the given identifier in the production GCS bucket. - */ - public static String createBlobKey(String identifier) { - return service().createGsBlobKey("/gs/" + Config.PRODUCTION_GCS_BUCKETNAME + "/" + identifier).getKeyString(); - } - - /** - * Serves the content of the file with the specified {@code fileKey} as the body of the given HTTP response. - */ - public static void serve(HttpServletResponse resp, String fileKey) throws IOException { - service().serve(new BlobKey(fileKey), resp); - } - -} diff --git a/src/main/java/teammates/logic/api/FileStorage.java b/src/main/java/teammates/logic/api/FileStorage.java new file mode 100644 index 00000000000..9cbe9a733e0 --- /dev/null +++ b/src/main/java/teammates/logic/api/FileStorage.java @@ -0,0 +1,51 @@ +package teammates.logic.api; + +import teammates.common.util.Config; +import teammates.logic.core.FileStorageService; +import teammates.logic.core.GoogleCloudStorageService; +import teammates.logic.core.LocalFileStorageService; + +/** + * Handles operations related to binary files. + */ +public class FileStorage { + + private final FileStorageService service; + + public FileStorage() { + if (Config.isDevServer()) { + service = new LocalFileStorageService(); + } else { + service = new GoogleCloudStorageService(); + } + } + + /** + * Returns true if a file with the specified {@code fileKey} exists in the storage. + */ + public boolean doesFileExist(String fileKey) { + return service.doesFileExist(fileKey); + } + + /** + * Gets the content of the file with the specified {@code fileKey} as bytes. + */ + public byte[] getContent(String fileKey) { + return service.getContent(fileKey); + } + + /** + * Deletes the file with the specified {@code fileKey}. + */ + public void delete(String fileKey) { + service.delete(fileKey); + } + + /** + * Creates a file with the specified {@code contentBytes} as content and with type {@code contentType}. + */ + public void create(String fileKey, byte[] contentBytes, String contentType) { + service.create(fileKey, contentBytes, contentType); + } + +} diff --git a/src/main/java/teammates/logic/api/Logic.java b/src/main/java/teammates/logic/api/Logic.java index 3769cde10ee..61e11edc5bf 100644 --- a/src/main/java/teammates/logic/api/Logic.java +++ b/src/main/java/teammates/logic/api/Logic.java @@ -107,31 +107,6 @@ public void deleteAccountCascade(String googleId) { accountsLogic.deleteAccountCascade(googleId); } - /** - * Delete the picture associated with the {@code key} in Cloud Storage. - * - *
Preconditions:
- * All parameters are non-null. - * - *

Fails silently if the {@code key} doesn't exist.

- */ - public void deletePicture(String key) { - Assumption.assertNotNull(key); - - profilesLogic.deletePicture(key); - } - - /** - * Deletes {@code pictureKey} for the student profile associated with {@code googleId}. - * - *

If the associated profile doesn't exist, create a new one.

- */ - public void deletePictureKey(String googleId) { - Assumption.assertNotNull(googleId); - - profilesLogic.deletePictureKey(googleId); - } - /** * Creates an instructor. * diff --git a/src/main/java/teammates/logic/core/FileStorageService.java b/src/main/java/teammates/logic/core/FileStorageService.java new file mode 100644 index 00000000000..58670b33a50 --- /dev/null +++ b/src/main/java/teammates/logic/core/FileStorageService.java @@ -0,0 +1,16 @@ +package teammates.logic.core; + +/** + * A binary file storage interface used for managing binary files such as profile pictures. + */ +public interface FileStorageService { + + boolean doesFileExist(String fileKey); + + byte[] getContent(String fileKey); + + void delete(String fileKey); + + void create(String fileKey, byte[] contentBytes, String contentType); + +} diff --git a/src/main/java/teammates/logic/core/GoogleCloudStorageService.java b/src/main/java/teammates/logic/core/GoogleCloudStorageService.java new file mode 100644 index 00000000000..8b83eae0f0b --- /dev/null +++ b/src/main/java/teammates/logic/core/GoogleCloudStorageService.java @@ -0,0 +1,47 @@ +package teammates.logic.core; + +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; + +import teammates.common.util.Config; + +/** + * Holds functions for operations related to Google Cloud Storage. + */ +public final class GoogleCloudStorageService implements FileStorageService { + + private static Storage storage = StorageOptions.getDefaultInstance().getService(); + + @Override + public void delete(String fileKey) { + storage.delete(BlobId.of(Config.PRODUCTION_GCS_BUCKETNAME, fileKey)); + } + + @Override + public void create(String fileKey, byte[] contentBytes, String contentType) { + BlobId blobId = BlobId.of(Config.PRODUCTION_GCS_BUCKETNAME, fileKey); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(contentType).build(); + storage.create(blobInfo, contentBytes); + } + + @Override + public boolean doesFileExist(String fileKey) { + BlobId blobId = BlobId.of(Config.PRODUCTION_GCS_BUCKETNAME, fileKey); + Blob blob = storage.get(blobId); + return blob.exists(); + } + + @Override + public byte[] getContent(String fileKey) { + BlobId blobId = BlobId.of(Config.PRODUCTION_GCS_BUCKETNAME, fileKey); + Blob blob = storage.get(blobId); + if (blob == null) { + return new byte[0]; + } + return blob.getContent(); + } + +} diff --git a/src/main/java/teammates/logic/core/LocalFileStorageService.java b/src/main/java/teammates/logic/core/LocalFileStorageService.java new file mode 100644 index 00000000000..a8c8741caf9 --- /dev/null +++ b/src/main/java/teammates/logic/core/LocalFileStorageService.java @@ -0,0 +1,57 @@ +package teammates.logic.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +import teammates.common.exception.TeammatesException; +import teammates.common.util.Logger; + +/** + * Holds functions for operations related to binary file storage in local dev environment. + */ +public final class LocalFileStorageService implements FileStorageService { + + private static final String BASE_DIRECTORY = "../../filestorage-dev"; + private static final Logger log = Logger.getLogger(); + + private static String constructFilePath(String fileKey) { + return BASE_DIRECTORY + "/" + fileKey; + } + + @Override + public void delete(String fileKey) { + File file = new File(constructFilePath(fileKey)); + file.delete(); + } + + @Override + public void create(String fileKey, byte[] contentBytes, String contentType) { + try (OutputStream os = Files.newOutputStream(Paths.get(constructFilePath(fileKey)))) { + os.write(contentBytes); + } catch (IOException e) { + log.warning(TeammatesException.toStringWithStackTrace(e)); + } + } + + @Override + public boolean doesFileExist(String fileKey) { + return Files.exists(Paths.get(constructFilePath(fileKey))); + } + + @Override + public byte[] getContent(String fileKey) { + byte[] buffer = new byte[1024 * 300]; + try (InputStream fis = Files.newInputStream(Paths.get(constructFilePath(fileKey)))) { + fis.read(buffer); + } catch (IOException e) { + log.warning(TeammatesException.toStringWithStackTrace(e)); + return new byte[0]; + } + return buffer; + } + +} diff --git a/src/main/java/teammates/logic/core/ProfilesLogic.java b/src/main/java/teammates/logic/core/ProfilesLogic.java index 4d5dd79a3ae..5a1cc315d80 100644 --- a/src/main/java/teammates/logic/core/ProfilesLogic.java +++ b/src/main/java/teammates/logic/core/ProfilesLogic.java @@ -50,22 +50,4 @@ public void deleteStudentProfile(String googleId) { profilesDb.deleteStudentProfile(googleId); } - /** - * Deletes picture associated with the {@code key}. - * - *

Fails silently if the {@code key} doesn't exist.

- */ - public void deletePicture(String key) { - profilesDb.deletePicture(key); - } - - /** - * Deletes {@code pictureKey} for the student profile associated with {@code googleId}. - * - *

If the associated profile doesn't exist, create a new one.

- */ - public void deletePictureKey(String googleId) { - profilesDb.deletePictureKey(googleId); - } - } diff --git a/src/main/java/teammates/storage/api/ProfilesDb.java b/src/main/java/teammates/storage/api/ProfilesDb.java index 7efbc5f9f40..53c94757d91 100644 --- a/src/main/java/teammates/storage/api/ProfilesDb.java +++ b/src/main/java/teammates/storage/api/ProfilesDb.java @@ -10,7 +10,6 @@ import teammates.common.datatransfer.attributes.StudentProfileAttributes; import teammates.common.exception.InvalidParametersException; import teammates.common.util.Assumption; -import teammates.common.util.GoogleCloudStorageHelper; import teammates.storage.entity.Account; import teammates.storage.entity.StudentProfile; @@ -62,8 +61,7 @@ public StudentProfileAttributes updateOrCreateStudentProfile(StudentProfileAttri && this.hasSameValue(studentProfile.getInstitute(), newAttributes.getInstitute()) && this.hasSameValue(studentProfile.getNationality(), newAttributes.getNationality()) && this.hasSameValue(studentProfile.getGender(), newAttributes.getGender().name().toLowerCase()) - && this.hasSameValue(studentProfile.getMoreInfo(), newAttributes.getMoreInfo()) - && this.hasSameValue(studentProfile.getPictureKey(), newAttributes.getPictureKey()); + && this.hasSameValue(studentProfile.getMoreInfo(), newAttributes.getMoreInfo()); if (!shouldCreateEntity && hasSameAttributes) { log.info(String.format(OPTIMIZED_SAVING_POLICY_APPLIED, StudentProfile.class.getSimpleName(), updateOptions)); return newAttributes; @@ -75,7 +73,6 @@ public StudentProfileAttributes updateOrCreateStudentProfile(StudentProfileAttri studentProfile.setNationality(newAttributes.nationality); studentProfile.setGender(newAttributes.gender.name().toLowerCase()); studentProfile.setMoreInfo(newAttributes.moreInfo); - studentProfile.setPictureKey(newAttributes.pictureKey); studentProfile.setModifiedDate(Instant.now()); saveEntity(studentProfile); @@ -93,39 +90,11 @@ public void deleteStudentProfile(String googleId) { if (sp == null) { return; } - if (!sp.getPictureKey().equals("")) { - deletePicture(sp.getPictureKey()); - } Key parentKey = Key.create(Account.class, googleId); Key profileKey = Key.create(parentKey, StudentProfile.class, googleId); deleteEntity(profileKey); } - /** - * Deletes picture associated with the {@code key}. - * - *

Fails silently if the {@code key} doesn't exist.

- */ - public void deletePicture(String key) { - GoogleCloudStorageHelper.deleteFile(key); - } - - /** - * Deletes the {@code pictureKey} of the profile with given {@code googleId} by setting it to an empty string. - * - *

Fails silently if the {@code studentProfile} doesn't exist.

- */ - public void deletePictureKey(String googleId) { - Assumption.assertNotNull(googleId); - StudentProfile studentProfile = getStudentProfileEntityFromDb(googleId); - - if (studentProfile != null) { - studentProfile.setPictureKey(""); - studentProfile.setModifiedDate(Instant.now()); - saveEntity(studentProfile); - } - } - /** * Gets the profile entity associated with the {@code googleId}. * diff --git a/src/main/java/teammates/storage/entity/StudentProfile.java b/src/main/java/teammates/storage/entity/StudentProfile.java index 7492893bac8..b6a9c4428a7 100644 --- a/src/main/java/teammates/storage/entity/StudentProfile.java +++ b/src/main/java/teammates/storage/entity/StudentProfile.java @@ -2,7 +2,6 @@ import java.time.Instant; -import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.datastore.Text; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; @@ -43,8 +42,6 @@ public class StudentProfile extends BaseEntity { @Unindex private Text moreInfo; - private BlobKey pictureKey; - @Index @Translate(InstantTranslatorFactory.class) private Instant modifiedDate; @@ -75,7 +72,7 @@ private StudentProfile() { * Miscellaneous information, including external profile */ public StudentProfile(String googleId, String shortName, String email, String institute, - String nationality, String gender, String moreInfo, String pictureKey) { + String nationality, String gender, String moreInfo) { this.setGoogleId(googleId); this.setShortName(shortName); this.setEmail(email); @@ -84,7 +81,6 @@ public StudentProfile(String googleId, String shortName, String email, String in this.setGender(gender); this.setMoreInfo(moreInfo); this.setModifiedDate(Instant.now()); - this.setPictureKey(pictureKey); } public StudentProfile(String googleId) { @@ -95,7 +91,6 @@ public StudentProfile(String googleId) { this.setNationality(""); this.setGender("other"); this.setMoreInfo(""); - this.setPictureKey(""); this.setModifiedDate(Instant.now()); } @@ -159,14 +154,6 @@ public void setMoreInfo(String moreInfo) { this.moreInfo = moreInfo == null ? null : new Text(moreInfo); } - public String getPictureKey() { - return this.pictureKey == null ? null : this.pictureKey.getKeyString(); - } - - public void setPictureKey(String pictureKey) { - this.pictureKey = pictureKey == null ? null : new BlobKey(pictureKey); - } - public Instant getModifiedDate() { return this.modifiedDate; } diff --git a/src/main/java/teammates/ui/output/StudentProfilePictureResults.java b/src/main/java/teammates/ui/output/StudentProfilePictureResults.java deleted file mode 100644 index 678b2adae4a..00000000000 --- a/src/main/java/teammates/ui/output/StudentProfilePictureResults.java +++ /dev/null @@ -1,16 +0,0 @@ -package teammates.ui.output; - -/** - * API output for profile picture results. - */ -public class StudentProfilePictureResults extends ApiOutput { - private final String pictureKey; - - public StudentProfilePictureResults(String pictureKey) { - this.pictureKey = pictureKey; - } - - public String getPictureKey() { - return pictureKey; - } -} diff --git a/src/main/java/teammates/ui/webapi/Action.java b/src/main/java/teammates/ui/webapi/Action.java index 36497693ac9..efdff2df0e5 100644 --- a/src/main/java/teammates/ui/webapi/Action.java +++ b/src/main/java/teammates/ui/webapi/Action.java @@ -22,6 +22,7 @@ import teammates.common.util.StringHelper; import teammates.logic.api.EmailGenerator; import teammates.logic.api.EmailSender; +import teammates.logic.api.FileStorage; import teammates.logic.api.GateKeeper; import teammates.logic.api.Logic; import teammates.logic.api.TaskQueuer; @@ -40,6 +41,7 @@ public abstract class Action { EmailGenerator emailGenerator = new EmailGenerator(); TaskQueuer taskQueuer = new TaskQueuer(); EmailSender emailSender = new EmailSender(); + FileStorage fileStorage = new FileStorage(); RecaptchaVerifier recaptchaVerifier = new RecaptchaVerifier(Config.CAPTCHA_SECRET_KEY); HttpServletRequest req; @@ -73,6 +75,14 @@ public void setEmailSender(EmailSender emailSender) { this.emailSender = emailSender; } + public FileStorage getFileStorage() { + return fileStorage; + } + + public void setFileStorage(FileStorage fileStorage) { + this.fileStorage = fileStorage; + } + public void setRecaptchaVerifier(RecaptchaVerifier recaptchaVerifier) { this.recaptchaVerifier = recaptchaVerifier; } diff --git a/src/main/java/teammates/ui/webapi/DeleteAccountAction.java b/src/main/java/teammates/ui/webapi/DeleteAccountAction.java index 0c40d6901b6..d7b957796c4 100644 --- a/src/main/java/teammates/ui/webapi/DeleteAccountAction.java +++ b/src/main/java/teammates/ui/webapi/DeleteAccountAction.java @@ -11,8 +11,11 @@ class DeleteAccountAction extends AdminOnlyAction { @Override JsonResult execute() { - String instructorId = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_ID); - logic.deleteAccountCascade(instructorId); + String googleId = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_ID); + if (fileStorage.doesFileExist(googleId)) { + fileStorage.delete(googleId); + } + logic.deleteAccountCascade(googleId); return new JsonResult("Account is successfully deleted.", HttpStatus.SC_OK); } diff --git a/src/main/java/teammates/ui/webapi/DeleteStudentProfilePictureAction.java b/src/main/java/teammates/ui/webapi/DeleteStudentProfilePictureAction.java index 734ee2b3e2b..f0723448757 100644 --- a/src/main/java/teammates/ui/webapi/DeleteStudentProfilePictureAction.java +++ b/src/main/java/teammates/ui/webapi/DeleteStudentProfilePictureAction.java @@ -7,7 +7,7 @@ import teammates.common.util.Const; /** - * Deletes a student's profile picture and its picture key. + * Deletes a student's profile picture. */ class DeleteStudentProfilePictureAction extends Action { @Override @@ -33,8 +33,9 @@ JsonResult execute() { if (studentProfileAttributes == null) { return new JsonResult("Invalid student profile", HttpStatus.SC_NOT_FOUND); } - logic.deletePicture(studentProfileAttributes.pictureKey); - logic.deletePictureKey(userInfo.id); + if (fileStorage.doesFileExist(studentProfileAttributes.googleId)) { + fileStorage.delete(studentProfileAttributes.googleId); + } return new JsonResult("Your profile picture has been deleted successfully", HttpStatus.SC_OK); } } diff --git a/src/main/java/teammates/ui/webapi/GetStudentProfilePictureAction.java b/src/main/java/teammates/ui/webapi/GetStudentProfilePictureAction.java index c0f76c885bf..bb3804e7fa9 100644 --- a/src/main/java/teammates/ui/webapi/GetStudentProfilePictureAction.java +++ b/src/main/java/teammates/ui/webapi/GetStudentProfilePictureAction.java @@ -63,10 +63,11 @@ ActionResult execute() { } } - if (studentProfile == null || studentProfile.pictureKey.equals("")) { + if (studentProfile == null || !fileStorage.doesFileExist(studentProfile.googleId)) { return new ImageResult(); } - return new ImageResult(studentProfile.pictureKey); + byte[] bytes = fileStorage.getContent(studentProfile.googleId); + return new ImageResult(bytes); } } diff --git a/src/main/java/teammates/ui/webapi/ImageResult.java b/src/main/java/teammates/ui/webapi/ImageResult.java index 0d22dea7f78..c5b96d665c6 100644 --- a/src/main/java/teammates/ui/webapi/ImageResult.java +++ b/src/main/java/teammates/ui/webapi/ImageResult.java @@ -6,35 +6,31 @@ import org.apache.http.HttpStatus; -import teammates.common.util.GoogleCloudStorageHelper; - /** * Action result in form of an image. */ class ImageResult extends ActionResult { - /** The blob key for the image. */ - private String blobKey; + private byte[] bytes; ImageResult() { super(HttpStatus.SC_NO_CONTENT); + this.bytes = new byte[0]; } - ImageResult(String blobKey) { + ImageResult(byte[] bytes) { super(HttpStatus.SC_OK); - this.blobKey = blobKey; + this.bytes = bytes; } - String getBlobKey() { - return blobKey; + byte[] getBytes() { + return this.bytes; } @Override void send(HttpServletResponse resp) throws IOException { resp.setContentType("image/png"); - if (blobKey != null) { - GoogleCloudStorageHelper.serve(resp, blobKey); - } + resp.getOutputStream().write(bytes); } } diff --git a/src/main/java/teammates/ui/webapi/PostStudentProfilePictureAction.java b/src/main/java/teammates/ui/webapi/PostStudentProfilePictureAction.java index 4959f30bd4c..7d12abda0d9 100644 --- a/src/main/java/teammates/ui/webapi/PostStudentProfilePictureAction.java +++ b/src/main/java/teammates/ui/webapi/PostStudentProfilePictureAction.java @@ -8,12 +8,8 @@ import org.apache.http.HttpStatus; -import teammates.common.datatransfer.attributes.StudentProfileAttributes; import teammates.common.exception.InvalidHttpRequestBodyException; -import teammates.common.exception.InvalidParametersException; import teammates.common.exception.UnauthorizedAccessException; -import teammates.common.util.GoogleCloudStorageHelper; -import teammates.ui.output.StudentProfilePictureResults; /** * Action: saves the file information of the profile picture that was just uploaded. @@ -53,16 +49,8 @@ JsonResult execute() { try (InputStream is = image.getInputStream()) { is.read(imageData); } - String pictureKey = GoogleCloudStorageHelper.writeImageDataToGcs(userInfo.id, imageData, image.getContentType()); - logic.updateOrCreateStudentProfile( - StudentProfileAttributes.updateOptionsBuilder(userInfo.id) - .withPictureKey(pictureKey) - .build()); - StudentProfilePictureResults dataFormat = - new StudentProfilePictureResults(pictureKey); - return new JsonResult(dataFormat); - } catch (InvalidParametersException ipe) { - throw new InvalidHttpRequestBodyException(ipe.getMessage(), ipe); + fileStorage.create(userInfo.id, imageData, image.getContentType()); + return new JsonResult("Your profile picture is updated successfully."); } catch (ServletException | IOException e) { return new JsonResult(e.getMessage(), HttpStatus.SC_INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/teammates/ui/webapi/ResetAccountAction.java b/src/main/java/teammates/ui/webapi/ResetAccountAction.java index fe43b82360f..85d62aacfbd 100644 --- a/src/main/java/teammates/ui/webapi/ResetAccountAction.java +++ b/src/main/java/teammates/ui/webapi/ResetAccountAction.java @@ -61,6 +61,9 @@ JsonResult execute() { if (wrongGoogleId != null && logic.getStudentsForGoogleId(wrongGoogleId).isEmpty() && logic.getInstructorsForGoogleId(wrongGoogleId).isEmpty()) { + if (fileStorage.doesFileExist(wrongGoogleId)) { + fileStorage.delete(wrongGoogleId); + } logic.deleteAccountCascade(wrongGoogleId); } diff --git a/src/main/java/teammates/ui/webapi/UpdateStudentProfileAction.java b/src/main/java/teammates/ui/webapi/UpdateStudentProfileAction.java index 686a45a5346..637ea4a6dc3 100644 --- a/src/main/java/teammates/ui/webapi/UpdateStudentProfileAction.java +++ b/src/main/java/teammates/ui/webapi/UpdateStudentProfileAction.java @@ -67,7 +67,6 @@ private StudentProfileAttributes extractProfileData(String studentId, StudentPro editedProfile.gender = StudentProfileAttributes.Gender.getGenderEnumValue(req.getGender()); editedProfile.moreInfo = req.getMoreInfo(); - editedProfile.pictureKey = ""; sanitizeProfile(editedProfile); return editedProfile; diff --git a/src/main/resources/InstructorSampleData.json b/src/main/resources/InstructorSampleData.json index d4db57a9d1f..58ef2d94ec3 100644 --- a/src/main/resources/InstructorSampleData.json +++ b/src/main/resources/InstructorSampleData.json @@ -3027,8 +3027,7 @@ "institute": "TEAMMATES Test Institute 5", "nationality": "Chinese", "gender": "FEMALE", - "moreInfo": "I am Alice from TEAMMATES Test Institute 1 and am here at Cambridge for exchange. My Major in TEAMMATES Test Institute 5 is Art History with Minor in Business Administration. I really look forward to a great and fruitful semester ahead!", - "pictureKey": "" + "moreInfo": "I am Alice from TEAMMATES Test Institute 1 and am here at Cambridge for exchange. My Major in TEAMMATES Test Institute 5 is Art History with Minor in Business Administration. I really look forward to a great and fruitful semester ahead!" }, "danny.e.tmms": { "googleId": "danny.e.tmms.sampleData", @@ -3037,8 +3036,7 @@ "institute": "TEAMMATES Test Institute 7", "nationality": "Singaporean", "gender": "MALE", - "moreInfo": "", - "pictureKey": "" + "moreInfo": "" }, "emma.f.tmms": { "googleId": "emma.f.tmms.sampleData", @@ -3047,8 +3045,7 @@ "institute": "TEAMMATES Test Institute 8", "nationality": "Indian", "gender": "FEMALE", - "moreInfo": "I am Emma from TEAMMATES Test Institute 8 India and am here at TEAMMATES Test Institute 5 for exchange. ", - "pictureKey": "" + "moreInfo": "I am Emma from TEAMMATES Test Institute 8 India and am here at TEAMMATES Test Institute 5 for exchange. " }, "charlie.d.tmms": { "googleId": "charlie.d.tmms.sampleData", @@ -3057,8 +3054,7 @@ "institute": "TEAMMATES Test Institute 8", "nationality": "American", "gender": "MALE", - "moreInfo": "I am Charlie - an exchange student", - "pictureKey": "" + "moreInfo": "I am Charlie - an exchange student" }, "gene.h.tmms@demo.course": { "googleId": "gene.h.tmms.sampleData", @@ -3067,8 +3063,7 @@ "institute": "TEAMMATES Test Institute 5", "nationality": "Nigerian", "gender": "FEMALE", - "moreInfo": "", - "pictureKey": "" + "moreInfo": "" } } } diff --git a/src/test/java/teammates/architecture/ArchitectureTest.java b/src/test/java/teammates/architecture/ArchitectureTest.java index eeda64c4a83..2f0be405fc8 100644 --- a/src/test/java/teammates/architecture/ArchitectureTest.java +++ b/src/test/java/teammates/architecture/ArchitectureTest.java @@ -455,30 +455,13 @@ public void testArchitecture_externalApi_searchApiCanOnlyBeAccessedBySomePackage } @Test - public void testArchitecture_externalApi_gcsApiCanOnlyBeAccessedByGcsHelper() { - noClasses().that().doNotHaveSimpleName("GoogleCloudStorageHelper") + public void testArchitecture_externalApi_cloudStorageApiCanOnlyBeAccessedByGcsService() { + noClasses().that().doNotHaveSimpleName("GoogleCloudStorageService") .and().resideOutsideOfPackage(includeSubpackages(CLIENT_SCRIPTS_PACKAGE)) - .should().accessClassesThat().resideInAPackage("com.google.appengine.tools.cloudstorage..") + .should().accessClassesThat().resideInAPackage("com.google.cloud.storage..") .check(ALL_CLASSES); } - @Test - public void testArchitecture_externalApi_blobstoreApiCanOnlyBeAccessedByGcsHelper() { - noClasses().that().doNotHaveSimpleName("GoogleCloudStorageHelper") - .and().resideOutsideOfPackage(includeSubpackages(STORAGE_ENTITY_PACKAGE)) - .should().accessClassesThat().resideInAPackage("com.google.appengine.api.blobstore..") - .check(ALL_CLASSES); - - noClasses().that().resideInAPackage(includeSubpackages(STORAGE_ENTITY_PACKAGE)) - .should().accessClassesThat(new DescribedPredicate("") { - @Override - public boolean apply(JavaClass input) { - return !"BlobKey".equals(input.getSimpleName()) - && input.getPackageName().startsWith("com.google.appengine.api.blobstore"); - } - }).check(ALL_CLASSES); - } - @Test public void testArchitecture_externalApi_cloudTasksApiCanOnlyBeAccessedByTaskQueueLogic() { noClasses().that().doNotHaveSimpleName("TaskQueuesLogic") @@ -513,8 +496,7 @@ public void testArchitecture_externalApi_datastoreTypesCanOnlyBeAccessedByEntity @Test public void testArchitecture_externalApi_servletApiCanOnlyBeAccessedBySomePackages() { - noClasses().that().doNotHaveSimpleName("GoogleCloudStorageHelper") - .and().doNotHaveSimpleName("HttpRequestHelper") + noClasses().that().doNotHaveSimpleName("HttpRequestHelper") .and().doNotHaveSimpleName("OfyHelper") .and().doNotHaveSimpleName("GaeSimulation") .and().doNotHaveSimpleName("MockFilterChain") diff --git a/src/test/java/teammates/common/datatransfer/attributes/StudentProfileAttributesTest.java b/src/test/java/teammates/common/datatransfer/attributes/StudentProfileAttributesTest.java index 9dee1464007..3f8b75964bd 100644 --- a/src/test/java/teammates/common/datatransfer/attributes/StudentProfileAttributesTest.java +++ b/src/test/java/teammates/common/datatransfer/attributes/StudentProfileAttributesTest.java @@ -29,7 +29,6 @@ public void classSetup() { .withNationality("Lebanese") .withGender(StudentProfileAttributes.Gender.FEMALE) .withMoreInfo("moreInfo can have a lot more than this...") - .withPictureKey("profile Pic Key") .build(); } @@ -44,7 +43,6 @@ public void testBuilder_withNothingPassed_shouldUseDefaultValues() { assertEquals("", profileAttributes.institute); assertEquals("", profileAttributes.nationality); assertEquals("", profileAttributes.moreInfo); - assertEquals("", profileAttributes.pictureKey); } @Test @@ -87,12 +85,6 @@ public void testBuilder_withNullValuePassed_shouldThrowException() { .withMoreInfo(null) .build(); }); - - assertThrows(AssertionError.class, () -> { - StudentProfileAttributes.builder(VALID_GOOGLE_ID) - .withPictureKey(null) - .build(); - }); } @Test @@ -104,7 +96,6 @@ public void testBuilder_withTypicalData_shouldBuildCorrectAttribute() { .withNationality("Lebanese") .withGender(StudentProfileAttributes.Gender.FEMALE) .withMoreInfo("moreInfo can have a lot more than this...") - .withPictureKey("profile Pic Key") .build(); assertEquals(VALID_GOOGLE_ID, studentProfileAttributes.getGoogleId()); @@ -114,14 +105,13 @@ public void testBuilder_withTypicalData_shouldBuildCorrectAttribute() { assertEquals("Lebanese", studentProfileAttributes.getNationality()); assertEquals(StudentProfileAttributes.Gender.FEMALE, studentProfileAttributes.getGender()); assertEquals("moreInfo can have a lot more than this...", studentProfileAttributes.getMoreInfo()); - assertEquals("profile Pic Key", studentProfileAttributes.getPictureKey()); } @Test public void testValueOf_withAllFieldPopulatedStudentProfile_shouldGenerateAttributesCorrectly() { StudentProfile studentProfile = new StudentProfile("id", "Joe", "joe@gmail.com", "Teammates Institute", "American", StudentProfileAttributes.Gender.MALE.name().toLowerCase(), - "hello", "key"); + "hello"); StudentProfileAttributes profileAttributes = StudentProfileAttributes.valueOf(studentProfile); assertEquals(studentProfile.getGoogleId(), profileAttributes.googleId); @@ -131,14 +121,13 @@ public void testValueOf_withAllFieldPopulatedStudentProfile_shouldGenerateAttrib assertEquals(studentProfile.getNationality(), profileAttributes.nationality); assertEquals(studentProfile.getGender(), profileAttributes.gender.name().toLowerCase()); assertEquals(studentProfile.getMoreInfo(), profileAttributes.moreInfo); - assertEquals(studentProfile.getPictureKey(), profileAttributes.pictureKey); } @Test public void testValueOf_withSomeFieldsPopulatedAsNull_shouldUseDefaultValues() { StudentProfile studentProfile = new StudentProfile("id", null, null, - null, null, null, null, null); + null, null, null, null); StudentProfileAttributes profileAttributes = StudentProfileAttributes.valueOf(studentProfile); assertEquals(studentProfile.getGoogleId(), profileAttributes.googleId); @@ -148,8 +137,6 @@ public void testValueOf_withSomeFieldsPopulatedAsNull_shouldUseDefaultValues() { assertEquals("", profileAttributes.nationality); assertEquals(StudentProfileAttributes.Gender.OTHER, profileAttributes.gender); assertEquals("", profileAttributes.moreInfo); - assertEquals("", profileAttributes.pictureKey); - } @Test @@ -204,7 +191,6 @@ public void testSanitizeForSaving() { assertEquals(profileToSanitizeExpected.nationality, profileToSanitize.nationality); assertEquals(profileToSanitizeExpected.gender, profileToSanitize.gender); assertEquals(profileToSanitizeExpected.moreInfo, profileToSanitize.moreInfo); - assertEquals(profileToSanitizeExpected.pictureKey, profileToSanitize.pictureKey); } @Override @@ -220,7 +206,6 @@ public void testToEntity() { assertEquals(expectedEntity.getNationality(), actualEntity.getNationality()); assertEquals(expectedEntity.getGender(), actualEntity.getGender()); assertEquals(expectedEntity.getMoreInfo(), actualEntity.getMoreInfo()); - assertEquals(expectedEntity.getPictureKey(), actualEntity.getPictureKey()); } @Test @@ -242,7 +227,6 @@ public void testUpdateOptions_withTypicalUpdateOptions_shouldUpdateAttributeCorr .withNationality("Singapore") .withGender(StudentProfileAttributes.Gender.MALE) .withMoreInfo("more info") - .withPictureKey("newPic") .build(); assertEquals("testGoogleId", updateOptions.getGoogleId()); @@ -257,7 +241,6 @@ public void testUpdateOptions_withTypicalUpdateOptions_shouldUpdateAttributeCorr assertEquals("Singapore", profileAttributes.nationality); assertEquals(StudentProfileAttributes.Gender.MALE, profileAttributes.gender); assertEquals("more info", profileAttributes.moreInfo); - assertEquals("newPic", profileAttributes.pictureKey); } @Test @@ -288,10 +271,6 @@ public void testUpdateOptionsBuilder_withNullInput_shouldFailWithAssertionError( assertThrows(AssertionError.class, () -> StudentProfileAttributes.updateOptionsBuilder("validId") .withMoreInfo(null)); - - assertThrows(AssertionError.class, () -> - StudentProfileAttributes.updateOptionsBuilder("validId") - .withPictureKey(null)); } @Test @@ -309,7 +288,6 @@ public void testEquals() { .withNationality("Lebanese") .withGender(StudentProfileAttributes.Gender.FEMALE) .withMoreInfo("moreInfo can have a lot more than this...") - .withPictureKey("profile Pic Key") .build(); assertTrue(profile.equals(studentProfileSimilar)); @@ -339,7 +317,6 @@ public void testHashCode() { .withNationality("Lebanese") .withGender(StudentProfileAttributes.Gender.FEMALE) .withMoreInfo("moreInfo can have a lot more than this...") - .withPictureKey("profile Pic Key") .build(); assertTrue(profile.hashCode() == studentProfileSimilar.hashCode()); @@ -359,7 +336,7 @@ private StudentProfile createStudentProfileFrom( StudentProfileAttributes profile) { return new StudentProfile(profile.googleId, profile.shortName, profile.email, profile.institute, profile.nationality, profile.gender.name().toLowerCase(), - profile.moreInfo, profile.pictureKey); + profile.moreInfo); } private List generatedExpectedErrorMessages(StudentProfileAttributes profile) throws Exception { @@ -395,7 +372,6 @@ private StudentProfileAttributes getInvalidStudentProfileAttributes() { String nationality = "$invalid nationality "; StudentProfileAttributes.Gender gender = StudentProfileAttributes.Gender.MALE; String moreInfo = "Ooops no validation for this one..."; - String pictureKey = ""; return StudentProfileAttributes.builder(googleId) .withShortName(shortName) @@ -404,7 +380,6 @@ private StudentProfileAttributes getInvalidStudentProfileAttributes() { .withNationality(nationality) .withGender(gender) .withMoreInfo(moreInfo) - .withPictureKey(pictureKey) .build(); } @@ -416,7 +391,6 @@ private StudentProfileAttributes getStudentProfileAttributesToSanitize() { String nationality = "&\"invalid nationality &"; StudentProfileAttributes.Gender gender = StudentProfileAttributes.Gender.OTHER; String moreInfo = "<"; - String pictureKey = "testPictureKey"; return StudentProfileAttributes.builder(googleId) .withShortName(shortName) @@ -425,7 +399,6 @@ private StudentProfileAttributes getStudentProfileAttributesToSanitize() { .withNationality(nationality) .withGender(gender) .withMoreInfo(moreInfo) - .withPictureKey(pictureKey) .build(); } diff --git a/src/test/java/teammates/common/util/BuildPropertiesTest.java b/src/test/java/teammates/common/util/BuildPropertiesTest.java index 25dbdc1c58d..c4d5b5fee6f 100644 --- a/src/test/java/teammates/common/util/BuildPropertiesTest.java +++ b/src/test/java/teammates/common/util/BuildPropertiesTest.java @@ -11,7 +11,7 @@ public class BuildPropertiesTest extends BaseTestCaseWithMinimalGaeEnvironment { @Test public void checkPresence() { - assertNotNull(Config.APP_URL); + assertNotNull(Config.getBaseAppUrl()); } } diff --git a/src/test/java/teammates/logic/core/ProfilesLogicTest.java b/src/test/java/teammates/logic/core/ProfilesLogicTest.java index 0b1ceae049d..cc83f58587c 100644 --- a/src/test/java/teammates/logic/core/ProfilesLogicTest.java +++ b/src/test/java/teammates/logic/core/ProfilesLogicTest.java @@ -48,29 +48,6 @@ public void testStudentProfileFunctions() throws Exception { expectedSpa.modifiedDate = actualSpa.modifiedDate; assertEquals(expectedSpa.toString(), actualSpa.toString()); assertEquals(expectedSpa.toString(), updateSpa.toString()); - - ______TS("update SP"); - - expectedSpa.pictureKey = "non-empty"; - profilesLogic.updateOrCreateStudentProfile( - StudentProfileAttributes.updateOptionsBuilder(expectedSpa.googleId) - .withPictureKey(expectedSpa.pictureKey) - .build()); - - actualSpa = profilesLogic.getStudentProfile(expectedSpa.googleId); - expectedSpa.modifiedDate = actualSpa.modifiedDate; - assertEquals(expectedSpa.toString(), actualSpa.toString()); - - ______TS("update picture"); - - expectedSpa.pictureKey = writeFileToGcs(expectedSpa.googleId, "src/test/resources/images/profile_pic.png"); - profilesLogic.updateOrCreateStudentProfile( - StudentProfileAttributes.updateOptionsBuilder(expectedSpa.googleId) - .withPictureKey(expectedSpa.pictureKey) - .build()); - actualSpa = profilesLogic.getStudentProfile(expectedSpa.googleId); - expectedSpa.modifiedDate = actualSpa.modifiedDate; - assertEquals(expectedSpa.toString(), actualSpa.toString()); } @Test @@ -80,24 +57,13 @@ public void testDeleteStudentProfile() throws Exception { profilesLogic.updateOrCreateStudentProfile( StudentProfileAttributes.updateOptionsBuilder("sp.logic.test") .withShortName("Test Name") - .withPictureKey(writeFileToGcs("sp.logic.test", "src/test/resources/images/profile_pic_default.png")) .build()); - // make sure we create an profile with picture key StudentProfileAttributes savedProfile = profilesLogic.getStudentProfile("sp.logic.test"); assertNotNull(savedProfile); - assertFalse(savedProfile.pictureKey.isEmpty()); profilesLogic.deleteStudentProfile("sp.logic.test"); // check that profile get deleted and picture get deleted verifyAbsentInDatastore(savedProfile); - assertFalse(doesFileExistInGcs(savedProfile.pictureKey)); - } - - @Test - public void testDeletePicture() throws Exception { - String keyString = writeFileToGcs("accountsLogicTestid", "src/test/resources/images/profile_pic.png"); - profilesLogic.deletePicture(keyString); - assertFalse(doesFileExistInGcs(keyString)); } } diff --git a/src/test/java/teammates/storage/api/ProfilesDbTest.java b/src/test/java/teammates/storage/api/ProfilesDbTest.java index 10452d28c7b..5e503611ee9 100644 --- a/src/test/java/teammates/storage/api/ProfilesDbTest.java +++ b/src/test/java/teammates/storage/api/ProfilesDbTest.java @@ -1,7 +1,5 @@ package teammates.storage.api; -import java.io.IOException; - import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -20,22 +18,15 @@ public class ProfilesDbTest extends BaseComponentTestCase { private StudentProfileAttributes typicalProfileWithPicture; private StudentProfileAttributes typicalProfileWithoutPicture; - private String typicalPictureKey; @BeforeMethod public void createTypicalData() throws Exception { - // typical picture - typicalPictureKey = uploadDefaultPictureForProfile("valid.googleId"); - assertTrue(doesFileExistInGcs(typicalPictureKey)); - // typical profiles profilesDb.createEntity(StudentProfileAttributes.builder("valid.googleId") .withInstitute("TEAMMATES Test Institute 1") - .withPictureKey(typicalPictureKey) .build()); profilesDb.createEntity(StudentProfileAttributes.builder("valid.googleId2") .withInstitute("TEAMMATES Test Institute 1") - .withPictureKey(typicalPictureKey) .build()); // save entity and picture @@ -50,10 +41,6 @@ public void deleteTypicalData() { profilesDb.deleteStudentProfile(typicalProfileWithoutPicture.googleId); verifyAbsentInDatastore(typicalProfileWithPicture); verifyAbsentInDatastore(typicalProfileWithoutPicture); - - // delete picture - profilesDb.deletePicture(typicalPictureKey); - assertFalse(doesFileExistInGcs(typicalPictureKey)); } @Test @@ -174,17 +161,6 @@ public void testUpdateOrCreateStudentProfile_updateSingleField_shouldUpdateCorre assertEquals("more info", updatedProfile.getMoreInfo()); assertEquals("more info", actualProfile.getMoreInfo()); assertEquals(actualProfile.getModifiedDate(), updatedProfile.getModifiedDate()); - - assertNotEquals("newPic", actualProfile.getPictureKey()); - updatedProfile = - profilesDb.updateOrCreateStudentProfile( - StudentProfileAttributes.updateOptionsBuilder(typicalProfileWithoutPicture.getGoogleId()) - .withPictureKey("newPic") - .build()); - actualProfile = profilesDb.getStudentProfile(typicalProfileWithoutPicture.getGoogleId()); - assertEquals("newPic", updatedProfile.getPictureKey()); - assertEquals("newPic", actualProfile.getPictureKey()); - assertEquals(actualProfile.getModifiedDate(), updatedProfile.getModifiedDate()); } @Test @@ -215,7 +191,6 @@ public void testUpdateOrCreateStudentProfile_noChangesToProfile_shouldNotIssueSa StudentProfileAttributes.updateOptionsBuilder(typicalProfileWithPicture.googleId) .withShortName(typicalProfileWithPicture.shortName) .withGender(typicalProfileWithPicture.gender) - .withPictureKey(typicalProfileWithPicture.pictureKey) .withMoreInfo(typicalProfileWithPicture.moreInfo) .withInstitute(typicalProfileWithPicture.institute) .withEmail(typicalProfileWithPicture.email) @@ -225,8 +200,6 @@ public void testUpdateOrCreateStudentProfile_noChangesToProfile_shouldNotIssueSa StudentProfileAttributes storedProfile = profilesDb.getStudentProfile(typicalProfileWithPicture.googleId); // other fields remain verifyPresentInDatastore(typicalProfileWithPicture); - // picture remains - assertTrue(doesFileExistInGcs(storedProfile.pictureKey)); // modifiedDate remains assertEquals(typicalProfileWithPicture.modifiedDate, storedProfile.modifiedDate); @@ -238,28 +211,10 @@ public void testUpdateOrCreateStudentProfile_noChangesToProfile_shouldNotIssueSa storedProfile = profilesDb.getStudentProfile(typicalProfileWithPicture.getGoogleId()); // other fields remain verifyPresentInDatastore(typicalProfileWithPicture); - // picture remains - assertTrue(doesFileExistInGcs(storedProfile.getPictureKey())); // modifiedDate remains assertEquals(typicalProfileWithPicture.getModifiedDate(), storedProfile.getModifiedDate()); } - @Test - public void testUpdateOrCreateStudentProfile_withNonEmptyPictureKey_shouldUpdateSuccessfully() throws Exception { - typicalProfileWithoutPicture.pictureKey = uploadDefaultPictureForProfile(typicalProfileWithPicture.googleId); - - StudentProfileAttributes updatedSpa = profilesDb.updateOrCreateStudentProfile( - StudentProfileAttributes.updateOptionsBuilder(typicalProfileWithoutPicture.googleId) - .withPictureKey(typicalProfileWithoutPicture.pictureKey) - .build()); - - verifyPresentInDatastore(typicalProfileWithoutPicture); - assertEquals(typicalProfileWithoutPicture.pictureKey, updatedSpa.pictureKey); - - // tear down - profilesDb.deletePicture(typicalProfileWithoutPicture.pictureKey); - } - @Test public void testDeleteStudentProfile_nonExistentEntity_shouldFailSilently() { profilesDb.deleteStudentProfile("test.non-existent"); @@ -280,30 +235,6 @@ public void testDeleteStudentProfile_profileWithPicture_shouldDeleteCorrectly() // check that profile get deleted and picture get deleted verifyAbsentInDatastore(typicalProfileWithPicture); - assertFalse(doesFileExistInGcs(typicalProfileWithPicture.pictureKey)); - } - - @Test - public void testDeletePicture_unknownBlobKey_shouldFailSilently() { - profilesDb.deletePicture("unknown"); - - assertFalse(doesFileExistInGcs("unknown")); } - @Test - public void testDeletePicture_typicalBlobKey_shouldDeleteSuccessfully() { - profilesDb.deletePicture(typicalPictureKey); - - assertFalse(doesFileExistInGcs(typicalPictureKey)); - } - - //------------------------------------------------------------------------------------------------------- - //-------------------------------------- Helper Functions ----------------------------------------------- - //------------------------------------------------------------------------------------------------------- - - private String uploadDefaultPictureForProfile(String googleId) - throws IOException { - // we upload a small text file as the actual file does not matter here - return writeFileToGcs(googleId, "src/test/resources/images/not_a_picture.txt"); - } } diff --git a/src/test/java/teammates/test/BaseComponentTestCase.java b/src/test/java/teammates/test/BaseComponentTestCase.java index 2d76785c7a6..894bcf523c9 100644 --- a/src/test/java/teammates/test/BaseComponentTestCase.java +++ b/src/test/java/teammates/test/BaseComponentTestCase.java @@ -20,7 +20,6 @@ import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.common.datatransfer.attributes.StudentProfileAttributes; import teammates.common.exception.TeammatesException; -import teammates.common.util.GoogleCloudStorageHelper; import teammates.common.util.retry.RetryManager; import teammates.logic.api.LogicExtension; @@ -33,6 +32,7 @@ public class BaseComponentTestCase extends BaseTestCaseWithDatastoreAccess { protected static final GaeSimulation gaeSimulation = GaeSimulation.inst(); protected static final LogicExtension logic = new LogicExtension(); + private static final MockFileStorage MOCK_FILE_STORAGE = new MockFileStorage(); @Override @BeforeClass @@ -51,14 +51,18 @@ protected RetryManager getPersistenceRetryManager() { return new RetryManager(TestProperties.PERSISTENCE_RETRY_PERIOD_IN_S / 2); } - protected static String writeFileToGcs(String googleId, String filename) throws IOException { - byte[] image = FileHelper.readFileAsBytes(filename); - String contentType = URLConnection.guessContentTypeFromName(filename); - return GoogleCloudStorageHelper.writeImageDataToGcs(googleId, image, contentType); + protected static void writeFileToStorage(String targetFileName, String sourceFilePath) throws IOException { + byte[] bytes = FileHelper.readFileAsBytes(sourceFilePath); + String contentType = URLConnection.guessContentTypeFromName(sourceFilePath); + MOCK_FILE_STORAGE.create(targetFileName, bytes, contentType); } - protected static boolean doesFileExistInGcs(String fileKey) { - return GoogleCloudStorageHelper.doesFileExistInGcs(fileKey); + protected static void deleteFile(String fileName) { + MOCK_FILE_STORAGE.delete(fileName); + } + + protected static boolean doesFileExist(String fileName) { + return MOCK_FILE_STORAGE.doesFileExist(fileName); } @Override diff --git a/src/test/java/teammates/test/BaseTestCase.java b/src/test/java/teammates/test/BaseTestCase.java index 4ed4fc039df..1b111c0d695 100644 --- a/src/test/java/teammates/test/BaseTestCase.java +++ b/src/test/java/teammates/test/BaseTestCase.java @@ -145,6 +145,10 @@ protected static void assertEquals(String message, Object expected, Object actua Assert.assertEquals(message, expected, actual); } + protected static void assertArrayEquals(byte[] expected, byte[] actual) { + Assert.assertArrayEquals(expected, actual); + } + protected static void assertNotEquals(Object first, Object second) { Assert.assertNotEquals(first, second); } diff --git a/src/test/java/teammates/test/EmailChecker.java b/src/test/java/teammates/test/EmailChecker.java index 4a2b8064630..1df51f10d09 100644 --- a/src/test/java/teammates/test/EmailChecker.java +++ b/src/test/java/teammates/test/EmailChecker.java @@ -63,7 +63,7 @@ private static String injectTestProperties(String emailContent) { } private static String getAppUrl() { - return Config.isDevServer() ? Config.APP_FRONTENDDEV_URL : Config.APP_URL; + return Config.getFrontEndAppUrl("").toAbsoluteString(); } /** diff --git a/src/test/java/teammates/test/EmailCheckerTest.java b/src/test/java/teammates/test/EmailCheckerTest.java index 2ef9237989a..0a2ddf38f87 100644 --- a/src/test/java/teammates/test/EmailCheckerTest.java +++ b/src/test/java/teammates/test/EmailCheckerTest.java @@ -26,7 +26,7 @@ private String injectContextDependentValuesForTest(String emailContent) { } private static String getAppUrl() { - return Config.isDevServer() ? Config.APP_FRONTENDDEV_URL : Config.APP_URL; + return Config.getFrontEndAppUrl("").toAbsoluteString(); } } diff --git a/src/test/java/teammates/test/FileHelper.java b/src/test/java/teammates/test/FileHelper.java index b3512832b76..8db38d29db6 100644 --- a/src/test/java/teammates/test/FileHelper.java +++ b/src/test/java/teammates/test/FileHelper.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Scanner; @@ -44,7 +45,15 @@ public static void saveFile(String filePath, String content) throws IOException try (BufferedWriter fw = Files.newBufferedWriter(Paths.get(filePath))) { fw.write(content); } + } + /** + * Saves the supplied content to the specified file path. + */ + public static void saveFile(String filePath, byte[] content) throws IOException { + try (OutputStream os = Files.newOutputStream(Paths.get(filePath))) { + os.write(content); + } } /** diff --git a/src/test/java/teammates/test/GaeSimulation.java b/src/test/java/teammates/test/GaeSimulation.java index de18712cb22..d0a4afebf4c 100644 --- a/src/test/java/teammates/test/GaeSimulation.java +++ b/src/test/java/teammates/test/GaeSimulation.java @@ -138,6 +138,7 @@ public Action getActionObject(String uri, String method, String body, Map invalidProfilePicAction.execute()); + + deleteFile(student1.googleId); } @Override diff --git a/src/test/resources/data/FeedbackSessionResultsBundleTest.json b/src/test/resources/data/FeedbackSessionResultsBundleTest.json index d3827e3576b..f0345de2e9d 100644 --- a/src/test/resources/data/FeedbackSessionResultsBundleTest.json +++ b/src/test/resources/data/FeedbackSessionResultsBundleTest.json @@ -282,8 +282,7 @@ "institute": "TEAMMATES Test Institute 1", "nationality": "American", "gender": "MALE", - "moreInfo": "I am just a student :P", - "pictureKey": "asdf34&hfn3!@" + "moreInfo": "I am just a student :P" } } } diff --git a/src/test/resources/data/FeedbackSessionsLogicTest.json b/src/test/resources/data/FeedbackSessionsLogicTest.json index bb916296268..8293cadf0fb 100644 --- a/src/test/resources/data/FeedbackSessionsLogicTest.json +++ b/src/test/resources/data/FeedbackSessionsLogicTest.json @@ -1403,8 +1403,7 @@ "institute": "TEAMMATES Test Institute 3", "nationality": "American", "gender": "MALE", - "moreInfo": "I am just a student :P", - "pictureKey": "asdf34&hfn3!@" + "moreInfo": "I am just a student :P" } } } diff --git a/src/test/resources/data/typicalDataBundle.json b/src/test/resources/data/typicalDataBundle.json index 60551c48a10..e432c41fc23 100644 --- a/src/test/resources/data/typicalDataBundle.json +++ b/src/test/resources/data/typicalDataBundle.json @@ -1685,8 +1685,7 @@ "institute": "TEAMMATES Test Institute 3", "nationality": "American", "gender": "MALE", - "moreInfo": "I am just a student :P", - "pictureKey": "asdf34&hfn3!@" + "moreInfo": "I am just a student :P" }, "student1InTestingSanitizationCourse": { "googleId": "student1InTestingSanitizationCourse", @@ -1695,8 +1694,7 @@ "institute": "inst", "nationality": "American", "gender": "OTHER", - "moreInfo": "I am just a student :P", - "pictureKey": "" + "moreInfo": "I am just a student :P" } } } diff --git a/src/test/resources/filestorage/.gitkeep b/src/test/resources/filestorage/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/resources/images/profile_pic_default.png b/src/test/resources/images/profile_pic_default.png deleted file mode 100644 index 818b23023c7..00000000000 Binary files a/src/test/resources/images/profile_pic_default.png and /dev/null differ diff --git a/src/web/app/pages-student/student-profile-page/student-profile-page.component.spec.ts b/src/web/app/pages-student/student-profile-page/student-profile-page.component.spec.ts index 546e45ab5e4..35ac3a22937 100644 --- a/src/web/app/pages-student/student-profile-page/student-profile-page.component.spec.ts +++ b/src/web/app/pages-student/student-profile-page/student-profile-page.component.spec.ts @@ -4,7 +4,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { environment } from '../../../environments/environment.prod'; -import { Gender } from '../../../types/api-output'; +import { Gender, StudentProfile } from '../../../types/api-output'; import { AjaxLoadingModule } from '../../components/ajax-loading/ajax-loading.module'; import { LoadingRetryModule } from '../../components/loading-retry/loading-retry.module'; import { LoadingSpinnerModule } from '../../components/loading-spinner/loading-spinner.module'; @@ -48,18 +48,14 @@ describe('StudentProfilePageComponent', () => { }); it('should snap with a student field without information', () => { - const studentDetails: any = { - studentProfile: { - shortName: '', - email: '', - institute: '', - nationality: '', - gender: Gender, - moreInfo: '', - pictureKey: '', - }, + const studentDetails: StudentProfile = { name: '', - requestId: '', + shortName: '', + email: '', + institute: '', + nationality: '', + gender: Gender.MALE, + moreInfo: '', }; component.student = studentDetails; component.editForm = new FormGroup({ @@ -77,18 +73,14 @@ describe('StudentProfilePageComponent', () => { }); it('should snap with values and a profile photo', () => { - const studentDetails: any = { - studentProfile: { - shortName: 'Ash', - email: 'ayush@nus.com', - institute: 'NUS', - nationality: 'Indian', - gender: Gender.MALE, - moreInfo: 'I like to party', - pictureKey: 'photo.jpg', - }, + const studentDetails: StudentProfile = { name: 'Ayush', - requestId: '16', + shortName: 'Ash', + email: 'ayush@nus.com', + institute: 'NUS', + nationality: 'Indian', + gender: Gender.MALE, + moreInfo: 'I like to party', }; component.student = studentDetails; component.profilePicLink = `${environment.backendUrl}/webapi/students/` + diff --git a/src/web/app/pages-student/student-profile-page/student-profile-page.component.ts b/src/web/app/pages-student/student-profile-page/student-profile-page.component.ts index f88336bc460..26a47ef3fb8 100644 --- a/src/web/app/pages-student/student-profile-page/student-profile-page.component.ts +++ b/src/web/app/pages-student/student-profile-page/student-profile-page.component.ts @@ -202,7 +202,7 @@ export class StudentProfilePageComponent implements OnInit { } /** - * Deletes the profile picture and the profile picture key + * Deletes the profile picture. */ deleteProfilePicture(): void { const paramMap: Record = { diff --git a/src/web/services/student-profile.service.ts b/src/web/services/student-profile.service.ts index d37c19746db..3af8f96c0de 100644 --- a/src/web/services/student-profile.service.ts +++ b/src/web/services/student-profile.service.ts @@ -57,7 +57,7 @@ export class StudentProfileService { } /** - * Deletes the profile picture and the profile picture key + * Deletes the profile picture. */ deleteProfilePicture(paramMap: Record): Observable { return this.httpRequestService.delete(ResourceEndpoints.STUDENT_PROFILE_PICTURE, paramMap);