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

43 events gallery of photos #203

Merged
merged 41 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6885bee
Adding gallery to event model and simple service methods
AvilaAndre Sep 16, 2023
6eca9e9
Uploading image
AvilaAndre Sep 26, 2023
d6a1f3d
removed code added by accident
AvilaAndre Oct 17, 2023
0e67a81
Changed galery image folder
AvilaAndre Oct 17, 2023
28c58f1
Created addPhoto test
AvilaAndre Oct 17, 2023
3f061dc
fixed photoUrl type and added errors when event/photo doesn't exist
AvilaAndre Oct 18, 2023
a8b0c9e
test photo add if event does not exist
AvilaAndre Oct 23, 2023
e2262dd
test photo add wrong image format test
AvilaAndre Oct 23, 2023
8a2f4b9
Remove photo from gallery test
AvilaAndre Oct 23, 2023
79ee8f1
gallery photo remove from unexistent event test
AvilaAndre Oct 23, 2023
c8d19ef
gallery remove unexistent photo test
AvilaAndre Oct 23, 2023
4b0634f
generalized gallery to activity
AvilaAndre Jan 30, 2024
50acea5
Move gallery service methods from Event to Activity
AvilaAndre Feb 4, 2024
91f0ea6
Merge branch 'develop' into 43-events-gallery-of-photos
AvilaAndre Feb 6, 2024
91ace92
added gallery to develop tests
AvilaAndre Feb 6, 2024
a3516e7
added gallery documentation to activity payload
AvilaAndre Feb 6, 2024
cd6f37f
Update src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt
AvilaAndre Mar 28, 2024
971b2c0
Added missing import
AvilaAndre Mar 28, 2024
6b3e978
Formatted imports
AvilaAndre Mar 28, 2024
c6f4c3c
Removed unnnecessary code
AvilaAndre Mar 28, 2024
31d5a4b
Replaced photo with image
AvilaAndre Mar 28, 2024
8639688
added image folder to gallery
AvilaAndre Mar 28, 2024
1863c6f
Updated gallery documentation
AvilaAndre Mar 28, 2024
250bb18
Fixing tests failing after changes
AvilaAndre Mar 29, 2024
dd4818b
replace endpoints to be restful and add delete method to testing
AvilaAndre Jul 20, 2024
1b664c1
generalized image to file
AvilaAndre Jul 20, 2024
02ced4e
added file deletion to FileUploader class and delete files on removal
AvilaAndre Jul 21, 2024
26df8a7
feat: serve static files on the /static/ path
AvilaAndre Oct 18, 2024
7d42ee7
tweak: change static port from 3000 to 8080 for development
AvilaAndre Oct 18, 2024
95f4a50
Merge branch 'develop' into 43-events-gallery-of-photos
AvilaAndre Oct 18, 2024
0c68988
fix: add gallery list to Auth test
AvilaAndre Oct 18, 2024
02f45a9
lint: line too long
AvilaAndre Oct 18, 2024
693afa8
Update src/main/kotlin/pt/up/fe/ni/website/backend/service/activity/A…
AvilaAndre Nov 6, 2024
c796bac
Update src/test/kotlin/pt/up/fe/ni/website/backend/utils/mockmvc/Mock…
AvilaAndre Nov 6, 2024
0b5c228
fix: Added explanation to line and replaced variable
AvilaAndre Nov 6, 2024
2735fdc
lint: missing space
AvilaAndre Nov 6, 2024
12d6616
docs: started documentation while #144 is not done
AvilaAndre Nov 6, 2024
2322b14
test: added gallery tests to Project
AvilaAndre Nov 6, 2024
aeef473
refactor: make slug non optional
AvilaAndre Nov 7, 2024
2f26ff6
Merge branch 'develop' into 43-events-gallery-of-photos
AvilaAndre Nov 7, 2024
88e915d
Merge branch 'develop' into 43-events-gallery-of-photos
MRita443 Nov 20, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ out/

### VS Code ###
.vscode/

### Static Content
static/
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.AuthenticationException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.multipart.MaxUploadSizeExceededException
import org.springframework.web.multipart.support.MissingServletRequestPartException
import org.springframework.web.servlet.NoHandlerFoundException
import pt.up.fe.ni.website.backend.config.Logging

data class SimpleError(
Expand All @@ -32,7 +32,7 @@ data class CustomError(val errors: List<SimpleError>)
@RestControllerAdvice
class ErrorController(private val objectMapper: ObjectMapper) : ErrorController, Logging {

@RequestMapping("/**")
@ExceptionHandler(NoHandlerFoundException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun endpointNotFound(): CustomError = wrapSimpleError("invalid endpoint")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,18 @@ class EventController(private val service: EventService) {
@PathVariable idEvent: Long,
@PathVariable idAccount: Long
) = service.removeTeamMemberById(idEvent, idAccount)

@PutMapping("/{idEvent}/gallery", consumes = ["multipart/form-data"])
fun addGalleryImage(
@PathVariable idEvent: Long,
@RequestParam
@ValidImage
image: MultipartFile
) = service.addGalleryImage(idEvent, image, EventService.IMAGE_FOLDER)

@DeleteMapping("/{idEvent}/gallery")
fun removeGalleryImage(
@PathVariable idEvent: Long,
@RequestPart imageUrl: String
) = service.removeGalleryImage(idEvent, imageUrl)
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,18 @@ class ProjectController(private val service: ProjectService) {
@PathVariable idProject: Long,
@PathVariable idAccount: Long
) = service.removeHallOfFameMemberById(idProject, idAccount)

@PutMapping("/{idProject}/gallery", consumes = ["multipart/form-data"])
fun addGalleryImage(
@PathVariable idProject: Long,
@RequestParam
@ValidImage
image: MultipartFile
) = service.addGalleryImage(idProject, image, ProjectService.IMAGE_FOLDER)

@DeleteMapping("/{idProject}/gallery")
fun removeGalleryImage(
@PathVariable idProject: Long,
@RequestPart imageUrl: String
) = service.removeGalleryImage(idProject, imageUrl)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ abstract class ActivityDto<T : Activity>(
val title: String,
val description: String,
val teamMembersIds: List<Long>?,
val slug: String?,
val slug: String,
var image: String?,
@JsonIgnore
var imageFile: MultipartFile? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class EventDto(
title: String,
description: String,
teamMembersIds: List<Long>?,
slug: String?,
slug: String,
image: String?,

val registerUrl: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ class PostDto(
val title: String,
val body: String,
val thumbnailPath: String,
val slug: String?
val slug: String
) : EntityDto<Post>()
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ProjectDto(
title: String,
description: String,
teamMembersIds: List<Long>?,
slug: String?,
slug: String,
image: String?,

val isArchived: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.ElementCollection
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
Expand Down Expand Up @@ -38,11 +39,14 @@ abstract class Activity(

@Column(unique = true)
@field:Size(min = Constants.Slug.minSize, max = Constants.Slug.maxSize)
open val slug: String? = null,
open val slug: String,

@field:NotBlank
open var image: String,

@ElementCollection(fetch = FetchType.EAGER)
open val gallery: MutableList<String> = mutableListOf(),

@Id
@GeneratedValue
open val id: Long? = null
Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class Event(
description: String,
teamMembers: MutableList<Account> = mutableListOf(),
associatedRoles: MutableList<PerActivityRole> = mutableListOf(),
slug: String? = null,
slug: String,
image: String,
gallery: MutableList<String> = mutableListOf(),

@field:NullOrNotBlank
@field:URL
Expand All @@ -33,4 +34,4 @@ class Event(
val category: String?,

id: Long? = null
) : Activity(title, description, teamMembers, associatedRoles, slug, image, id)
) : Activity(title, description, teamMembers, associatedRoles, slug, image, gallery, id)
2 changes: 1 addition & 1 deletion src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ class Post(

@Column(unique = true)
@field:Size(min = Constants.Slug.minSize, max = Constants.Slug.maxSize)
val slug: String? = null
val slug: String
)
5 changes: 3 additions & 2 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ class Project(
description: String,
teamMembers: MutableList<Account> = mutableListOf(),
associatedRoles: MutableList<PerActivityRole> = mutableListOf(),
slug: String? = null,
slug: String,
image: String,
gallery: MutableList<String> = mutableListOf(),

var isArchived: Boolean = false,

Expand Down Expand Up @@ -48,4 +49,4 @@ class Project(
val timeline: List<@Valid TimelineEvent> = emptyList(),

id: Long? = null
) : Activity(title, description, teamMembers, associatedRoles, slug, image, id)
) : Activity(title, description, teamMembers, associatedRoles, slug, image, gallery, id)
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import pt.up.fe.ni.website.backend.model.Activity

@Repository
interface ActivityRepository<T : Activity> : CrudRepository<T, Long> {
fun findBySlug(slug: String?): T?
fun findBySlug(slug: String): T?
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import pt.up.fe.ni.website.backend.model.Post

@Repository
interface PostRepository : JpaRepository<Post, Long> {
fun findBySlug(slug: String?): Post?
fun findBySlug(slug: String): Post?
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ object ErrorMessages {

fun roleNotFound(id: Long): String = "role not found with id $id"

fun fileNotFound(fileName: String): String = "file not found with name $fileName"

fun userAlreadyHasRole(roleId: Long, userId: Long): String = "user $userId already has role $roleId"

fun userNotInRole(roleId: Long, userId: Long): String = "user $userId doesn't have role $roleId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pt.up.fe.ni.website.backend.service.activity

import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import pt.up.fe.ni.website.backend.dto.entity.ActivityDto
import pt.up.fe.ni.website.backend.model.Activity
import pt.up.fe.ni.website.backend.repository.ActivityRepository
Expand All @@ -26,7 +27,7 @@ abstract class AbstractActivityService<T : Activity>(
}

dto.imageFile?.let {
val fileName = fileUploader.buildFileName(it, dto.title)
val fileName = fileUploader.buildFileName(it, dto.slug)
dto.image = fileUploader.uploadImage(imageFolder, fileName, it.bytes)
}

Expand All @@ -51,7 +52,7 @@ abstract class AbstractActivityService<T : Activity>(
if (imageFile == null) {
dto.image = activity.image
} else {
val fileName = fileUploader.buildFileName(imageFile, dto.title)
val fileName = fileUploader.buildFileName(imageFile, dto.slug)
dto.image = fileUploader.uploadImage(imageFolder, fileName, imageFile.bytes)
}

Expand All @@ -76,13 +77,33 @@ abstract class AbstractActivityService<T : Activity>(
fun removeTeamMemberById(idActivity: Long, idAccount: Long): T {
val activity = getActivityById(idActivity)
if (!accountService.doesAccountExist(idAccount)) {
throw NoSuchElementException(
ErrorMessages.accountNotFound(
idAccount
)
)
throw NoSuchElementException(ErrorMessages.accountNotFound(idAccount))
}
activity.teamMembers.removeIf { it.id == idAccount }
return repository.save(activity)
}

fun addGalleryImage(activityId: Long, image: MultipartFile, imageFolder: String): Activity {
val activity = getActivityById(activityId)

val fileName = fileUploader.buildFileName(image, activity.slug)
val imageName = fileUploader.uploadImage("$imageFolder/gallery", fileName, image.bytes)

activity.gallery.add(imageName)

return repository.save(activity)
}

fun removeGalleryImage(activityId: Long, imageName: String): Activity {
AvilaAndre marked this conversation as resolved.
Show resolved Hide resolved
val activity = getActivityById(activityId)

if (activity.gallery.contains(imageName)) {
fileUploader.deleteImage(imageName)
activity.gallery.remove(imageName)
} else {
throw NoSuchElementException(ErrorMessages.fileNotFound(imageName))
}

return repository.save(activity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ class CloudinaryFileUploader(private val basePath: String, private val cloudinar

return result["url"]?.toString() ?: ""
}

override fun deleteImage(fileName: String) {
// Here, fileName is the publicId in Cloudinary, which would be the path to the file
cloudinary.uploader().destroy(
fileName,
MRita443 marked this conversation as resolved.
Show resolved Hide resolved
null
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import pt.up.fe.ni.website.backend.utils.extensions.filenameExtension

abstract class FileUploader {
abstract fun uploadImage(folder: String, fileName: String, image: ByteArray): String
abstract fun deleteImage(fileName: String)

fun buildFileName(photoFile: MultipartFile, prefix: String = ""): String {
val limitedPrefix = prefix.take(100) // File name length has a limit of 256 characters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ class StaticFileUploader(private val storePath: String, private val servePath: S

return "$servePath/$folder/$fileName"
}

override fun deleteImage(fileName: String) {
val storedFileName = fileName.replace(servePath, storePath)
val file = File(storedFileName)

if (file.exists()) file.delete()
}
}
9 changes: 7 additions & 2 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ upload.provider=static
upload.cloudinary-base-path=website
upload.cloudinary-url=GET_YOURS_AT_CLOUDINARY_DASHBOARD
# Folder in which files will be stored
upload.static-path=classpath:static
upload.static-path=file:static
# URL that will serve static content
upload.static-serve=http://localhost:3000/static
upload.static-serve=http://localhost:8080/static

# Files are served in the following path
spring.mvc.static-path-pattern=/static/**
MRita443 marked this conversation as resolved.
Show resolved Hide resolved
# Add NoHandlerFoundException as an exception
spring.mvc.throw-exception-if-no-handler-found=true

# Cors Origin
cors.allow-origin = http://localhost:3000
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ class AuthControllerTest @Autowired constructor(
inner class CheckPermissions {
private val testPermissions = listOf(Permission.CREATE_ACCOUNT, Permission.CREATE_ACTIVITY)
private val testActivity = Project(
"Test Activity", "Test Description", mutableListOf(), mutableListOf(), "test slug", "test image", false,
emptyList(), null, "test target audience"
"Test Activity", "Test Description", mutableListOf(), mutableListOf(), "test slug", "test image",
mutableListOf(), false, emptyList(), null, "test target audience"
)
private val testRole = Role("MEMBER", Permissions(testPermissions), false)
private val testPerActivityRole = PerActivityRole(Permissions(listOf(Permission.EDIT_ACTIVITY)))
Expand Down
Loading
Loading