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

Added project missing fields #178

Merged
merged 24 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3111d56
Added image, slogan and targetAudience field to projects
BrunoRosendo Jul 19, 2023
b40047b
Changed request part name from dto to entity name
BrunoRosendo Jul 20, 2023
ef444c5
Added links field to projects
BrunoRosendo Jul 20, 2023
347a802
Added timeline to projects
BrunoRosendo Jul 20, 2023
3f644b1
Removed event thumbnail path
BrunoRosendo Jul 21, 2023
0d37520
Fixed account tests with field changes
BrunoRosendo Jul 21, 2023
2ed7f19
Added missing test to account controller
BrunoRosendo Jul 21, 2023
2130a66
Added documentation for new activity fields
BrunoRosendo Jul 21, 2023
0ad9ce8
Added tests when account part is missing
BrunoRosendo Jul 21, 2023
be5ea76
Updated event controller tests
BrunoRosendo Jul 21, 2023
66a20e0
Updated project controller tests
BrunoRosendo Jul 22, 2023
7b1a665
Testing team updates in PUT methods
BrunoRosendo Jul 22, 2023
ae787d9
Refactored activity services to avoid code duplication
BrunoRosendo Jul 25, 2023
bd710ff
Added github field to projects
BrunoRosendo Jul 31, 2023
7cb9ec9
Comparing activity slugs first to avoid fetch
BrunoRosendo Jul 31, 2023
8bede0c
Refactored account test mocks
BrunoRosendo Jul 31, 2023
8b41290
Fixed wrong optional fields in project payload
BrunoRosendo Aug 1, 2023
a3449e9
Removed wildcard imports
BrunoRosendo Aug 1, 2023
5fcb686
Creating upload directory if it doesn't exist
BrunoRosendo Aug 2, 2023
c8e6c5d
Added webp file type
BrunoRosendo Aug 2, 2023
a586226
Fixed malformed account test
BrunoRosendo Aug 2, 2023
8027707
Replaced wildcard import
BrunoRosendo Aug 7, 2023
511e498
Fixed wrong team in project test
BrunoRosendo Aug 7, 2023
05a4181
Updating hall of fame when updating projects
BrunoRosendo Aug 7, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,27 @@ class AccountController(private val service: AccountService) {
@GetMapping("/{id}")
fun getAccountById(@PathVariable id: Long) = service.getAccountById(id)

@PostMapping("/changePassword/{id}")
fun changePassword(@PathVariable id: Long, @RequestBody dto: ChangePasswordDto): Map<String, String> {
service.changePassword(id, dto)
return emptyMap()
@PostMapping("/new", consumes = ["multipart/form-data"])
fun createAccount(
@RequestPart account: CreateAccountDto,
BrunoRosendo marked this conversation as resolved.
Show resolved Hide resolved
@RequestParam
@ValidImage
photo: MultipartFile?
): Account {
account.photoFile = photo
return service.createAccount(account)
}

@PutMapping("/{id}")
@PutMapping("/{id}", consumes = ["multipart/form-data"])
fun updateAccountById(
@PathVariable id: Long,
@RequestPart dto: UpdateAccountDto,
@RequestPart account: UpdateAccountDto,
@RequestParam
@ValidImage
photo: MultipartFile?
): Account {
dto.photoFile = photo
return service.updateAccountById(id, dto)
account.photoFile = photo
return service.updateAccountById(id, account)
}

@DeleteMapping("/{id}")
Expand All @@ -53,14 +58,9 @@ class AccountController(private val service: AccountService) {
return emptyMap()
}

@PostMapping("/new", consumes = ["multipart/form-data"])
fun createAccount(
@RequestPart dto: CreateAccountDto,
@RequestParam
@ValidImage
photo: MultipartFile?
): Account {
dto.photoFile = photo
return service.createAccount(dto)
@PostMapping("/changePassword/{id}")
fun changePassword(@PathVariable id: Long, @RequestBody dto: ChangePasswordDto): Map<String, String> {
service.changePassword(id, dto)
return emptyMap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 pt.up.fe.ni.website.backend.config.Logging

data class SimpleError(
Expand Down Expand Up @@ -67,6 +68,12 @@ class ErrorController(private val objectMapper: ObjectMapper) : ErrorController,
return CustomError(errors)
}

@ExceptionHandler(MissingServletRequestPartException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun missingPart(e: MissingServletRequestPartException): CustomError {
return wrapSimpleError("required", param = e.requestPartName)
}

@ExceptionHandler(HttpMessageNotReadableException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun invalidRequestBody(e: HttpMessageNotReadableException): CustomError {
Expand Down Expand Up @@ -119,7 +126,7 @@ class ErrorController(private val objectMapper: ObjectMapper) : ErrorController,
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun unexpectedError(e: Exception): CustomError {
logger.error(e.message)
return wrapSimpleError("unexpected error: " + e.message)
return wrapSimpleError("unexpected error")
BrunoRosendo marked this conversation as resolved.
Show resolved Hide resolved
}

@ExceptionHandler(AccessDeniedException::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import pt.up.fe.ni.website.backend.dto.entity.EventDto
import pt.up.fe.ni.website.backend.model.Event
import pt.up.fe.ni.website.backend.service.activity.EventService
import pt.up.fe.ni.website.backend.utils.validation.ValidImage

@RestController
@RequestMapping("/events")
@Validated
BrunoRosendo marked this conversation as resolved.
Show resolved Hide resolved
class EventController(private val service: EventService) {
@GetMapping
fun getAllEvents() = service.getAllEvents()
Expand All @@ -26,20 +32,34 @@ class EventController(private val service: EventService) {
@GetMapping("/{eventSlug}**")
fun getEvent(@PathVariable eventSlug: String) = service.getEventBySlug(eventSlug)

@PostMapping("/new")
fun createEvent(@RequestBody dto: EventDto) = service.createEvent(dto)
@PostMapping("/new", consumes = ["multipart/form-data"])
fun createEvent(
@RequestPart event: EventDto,
@RequestParam
@ValidImage
image: MultipartFile
): Event {
event.imageFile = image
return service.createEvent(event)
}

@DeleteMapping("/{id}")
fun deleteEventById(@PathVariable id: Long): Map<String, String> {
service.deleteEventById(id)
return emptyMap()
}

@PutMapping("/{id}")
@PutMapping("/{id}", consumes = ["multipart/form-data"])
fun updateEventById(
@PathVariable id: Long,
@RequestBody dto: EventDto
) = service.updateEventById(id, dto)
@RequestPart event: EventDto,
@RequestParam
@ValidImage
image: MultipartFile?
): Event {
event.imageFile = image
return service.updateEventById(id, event)
}

@PutMapping("/{idEvent}/addTeamMember/{idAccount}")
fun addTeamMemberById(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import pt.up.fe.ni.website.backend.dto.entity.ProjectDto
import pt.up.fe.ni.website.backend.model.Project
import pt.up.fe.ni.website.backend.service.activity.ProjectService
import pt.up.fe.ni.website.backend.utils.validation.ValidImage

@RestController
@RequestMapping("/projects")
@Validated
class ProjectController(private val service: ProjectService) {

@GetMapping
Expand All @@ -24,20 +30,34 @@ class ProjectController(private val service: ProjectService) {
@GetMapping("/{projectSlug}**")
fun getProjectBySlug(@PathVariable projectSlug: String) = service.getProjectBySlug(projectSlug)

@PostMapping("/new")
fun createNewProject(@RequestBody dto: ProjectDto) = service.createProject(dto)
@PostMapping("/new", consumes = ["multipart/form-data"])
fun createProject(
@RequestPart project: ProjectDto,
@RequestParam
@ValidImage
image: MultipartFile
): Project {
project.imageFile = image
return service.createProject(project)
}

@DeleteMapping("/{id}")
fun deleteProjectById(@PathVariable id: Long): Map<String, String> {
service.deleteProjectById(id)
return emptyMap()
}

@PutMapping("/{id}")
@PutMapping("/{id}", consumes = ["multipart/form-data"])
fun updateProjectById(
@PathVariable id: Long,
@RequestBody dto: ProjectDto
) = service.updateProjectById(id, dto)
@RequestPart project: ProjectDto,
@RequestParam
@ValidImage
image: MultipartFile?
): Project {
project.imageFile = image
return service.updateProjectById(id, project)
}
Comment on lines +50 to +60
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this version, the way to update the timeline is to send a PUT with the whole project, the same way we do for the remaining fields. I think this makes the API simpler and is fine since the timeline doesn't hold a lot of data


@PutMapping("/{id}/archive")
fun archiveProjectById(@PathVariable id: Long) = service.archiveProjectById(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pt.up.fe.ni.website.backend.dto.entity

import com.fasterxml.jackson.annotation.JsonIgnore
import org.springframework.web.multipart.MultipartFile
import pt.up.fe.ni.website.backend.model.Activity

abstract class ActivityDto<T : Activity>(
val title: String,
val description: String,
val teamMembersIds: List<Long>?,
val slug: String?,
var image: String?,
@JsonIgnore
var imageFile: MultipartFile? = null
) : EntityDto<T>()
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import pt.up.fe.ni.website.backend.model.CustomWebsite

class CustomWebsiteDto(
val url: String,
val iconPath: String?
val iconPath: String?,
val label: String?
) : EntityDto<CustomWebsite>()
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import jakarta.persistence.Entity
import jakarta.validation.ConstraintViolationException
import jakarta.validation.Validator
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.jvmErasure
import pt.up.fe.ni.website.backend.config.ApplicationContextUtils

Expand Down Expand Up @@ -57,9 +59,13 @@ abstract class EntityDto<T : Any> {
// The use of suppress is explained at https://github.com/NIAEFEUP/website-niaefeup-backend/pull/20#discussion_r985236224
@Suppress("UNCHECKED_CAST")
private fun <T : Any> getTypeConversionClass(clazz: KClass<out EntityDto<T>>): KClass<T>? {
val thisType = clazz.supertypes.first { it.classifier == EntityDto::class }
val superType = clazz.supertypes.firstOrNull { it.jvmErasure.isSubclassOf(EntityDto::class) } ?: return null
val conversionClassArg =
superType.arguments.firstOrNull {
it.type?.jvmErasure?.findAnnotation<Entity>() != null
} ?: return getTypeConversionClass(superType.jvmErasure as KClass<out EntityDto<T>>)

return thisType.arguments.firstOrNull()?.type?.jvmErasure as KClass<T>?
return conversionClassArg.type?.jvmErasure as KClass<T>?
BrunoRosendo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import pt.up.fe.ni.website.backend.model.Event
import pt.up.fe.ni.website.backend.model.embeddable.DateInterval

class EventDto(
val title: String,
val description: String,
val teamMembersIds: List<Long>?,
title: String,
description: String,
teamMembersIds: List<Long>?,
slug: String?,
image: String?,

val registerUrl: String?,
val dateInterval: DateInterval,
val location: String?,
val category: String?,
val thumbnailPath: String,
val slug: String?
) : EntityDto<Event>()
val category: String?
) : ActivityDto<Event>(title, description, teamMembersIds, slug, image)
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ package pt.up.fe.ni.website.backend.dto.entity
import pt.up.fe.ni.website.backend.model.Project

class ProjectDto(
val title: String,
val description: String,
var hallOfFameIds: List<Long>?,
val teamMembersIds: List<Long>?,
title: String,
description: String,
teamMembersIds: List<Long>?,
slug: String?,
image: String?,

val isArchived: Boolean = false,
val technologies: List<String> = emptyList(),
val slug: String?
) : EntityDto<Project>()
val slogan: String?,
val targetAudience: String,
val github: String?,
val links: List<CustomWebsiteDto>?,
val timeline: List<TimelineEventDto>?,
val hallOfFameIds: List<Long>?
) : ActivityDto<Project>(title, description, teamMembersIds, slug, image)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pt.up.fe.ni.website.backend.dto.entity

import java.util.Date
import pt.up.fe.ni.website.backend.model.TimelineEvent

class TimelineEventDto(
val date: Date,
val description: String
) : EntityDto<TimelineEvent>()
4 changes: 4 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToMany
import jakarta.persistence.OrderColumn
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import pt.up.fe.ni.website.backend.model.constants.ActivityConstants as Constants

Expand Down Expand Up @@ -41,6 +42,9 @@ abstract class Activity(
@field:Size(min = Constants.Slug.minSize, max = Constants.Slug.maxSize)
open val slug: String? = null,

@field:NotBlank
open var image: String,

@Id
@GeneratedValue
open val id: Long? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class CustomWebsite(
@field:URL
val iconPath: String?,

@field:NullOrNotBlank
val label: String?,

@Id @GeneratedValue
val id: Long? = null
)
10 changes: 2 additions & 8 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.validation.Valid
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.Size
import org.hibernate.validator.constraints.URL
import pt.up.fe.ni.website.backend.model.constants.EventConstants as Constants
Expand All @@ -18,6 +16,7 @@ class Event(
teamMembers: MutableList<Account> = mutableListOf(),
associatedRoles: MutableList<PerActivityRole> = mutableListOf(),
slug: String? = null,
image: String,

@field:NullOrNotBlank
@field:URL
Expand All @@ -33,10 +32,5 @@ class Event(
@field:Size(min = Constants.Category.minSize, max = Constants.Category.maxSize)
val category: String?,

@JsonProperty(required = true)
@field:NotEmpty
@field:URL
val thumbnailPath: String,

id: Long? = null
) : Activity(title, description, teamMembers, associatedRoles, slug, id)
) : Activity(title, description, teamMembers, associatedRoles, slug, image, id)
Loading