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

Feature/generations #88

Merged
merged 37 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
479fb26
Added model for generations
BrunoRosendo Jan 22, 2023
9cdaca4
Changed role related lists to mutable
BrunoRosendo Jan 26, 2023
4ac2251
Added @Repository annotation to all repositories
BrunoRosendo Jan 22, 2023
b7862e3
Created endpoints to get generations
BrunoRosendo Jan 26, 2023
84551ce
Fixed empty sections being returned
BrunoRosendo Jan 26, 2023
57d2770
Created tests for SchoolYear validator and GetGenerationDto
BrunoRosendo Jan 26, 2023
dcb6d76
Added methods for creating and updating generations
BrunoRosendo Jan 31, 2023
50f48f8
Handle circular dependencies between relationships
BrunoRosendo Jan 31, 2023
ba5510f
Associated Dto IDs to respective entities
BrunoRosendo Jan 31, 2023
cc2acc7
Fixed error when getting generations
BrunoRosendo Jan 31, 2023
e9b844c
Changed types according to previous changes
BrunoRosendo Jan 31, 2023
bbe8b2d
Consistently using var for mutable fields and val for constant ones
BrunoRosendo Jan 31, 2023
3ca2925
Added annotation to validate duplicate roles
BrunoRosendo Jan 31, 2023
d7fd871
Fixed error when serializing constraint validation
BrunoRosendo Jan 31, 2023
1e0b500
Validating subsequent school years
BrunoRosendo Feb 4, 2023
294932a
Only include user in generation section if it's not included in previ…
BrunoRosendo Feb 5, 2023
1d20818
Added all account information to GetGenerationDto
BrunoRosendo Feb 5, 2023
480968e
Fixed wrong error in dto required properties
BrunoRosendo Feb 5, 2023
37932a1
Moves emptyMap() from services to controllers in void operations
BrunoRosendo Feb 6, 2023
47f6596
Added delete endpoints
BrunoRosendo Feb 6, 2023
6d39ba6
Fixed bi-directional relationships, incorrectly mapped as 2 one way r…
BrunoRosendo Feb 6, 2023
aac3795
Added roles order to relationships
BrunoRosendo Feb 6, 2023
4f0ed46
Ordered generations by school year
BrunoRosendo Feb 17, 2023
7c85152
Fixed get generation by id name
BrunoRosendo Feb 17, 2023
0e267a8
Fixed schoolYear not validated on update
BrunoRosendo Feb 17, 2023
9ba52af
GenerationController tests for get and patch endpoints
BrunoRosendo Feb 17, 2023
ab7a750
Fixed removal of roles when deleting a generation
BrunoRosendo Feb 17, 2023
411157b
Added delete endpoint tests for generations
BrunoRosendo Feb 17, 2023
256a0c5
Replaced "users" with "accounts" in GetGenerationDto
BrunoRosendo Feb 17, 2023
e3e28a3
Changed project service to general activity service in generations
BrunoRosendo Feb 20, 2023
a4f223d
Infer new generation's year if not specified
BrunoRosendo Feb 20, 2023
f76e103
Added tests for creating generations
BrunoRosendo Feb 21, 2023
3a63328
Tested cascading operations
BrunoRosendo Feb 21, 2023
29c0a83
Testing error if no generations when getting latest one
BrunoRosendo Feb 21, 2023
53e836b
Return emptyMap() in event controller instead of service
BrunoRosendo Mar 6, 2023
81dfa94
Removed useless open keywords in Activity
BrunoRosendo Mar 6, 2023
a7e2a1b
Fixed lint and README info
BrunoRosendo Mar 6, 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
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Website NIAEFEUP - BackEnd
[![codecov](https://codecov.io/gh/NIAEFEUP/website-niaefeup-backend/branch/develop/graph/badge.svg?token=4OPGXYESGP)](https://codecov.io/gh/NIAEFEUP/website-niaefeup-backend)
# Website NIAEFEUP - BackEnd
[![codecov](https://codecov.io/gh/NIAEFEUP/website-niaefeup-backend/branch/develop/graph/badge.svg?token=4OPGXYESGP)](https://codecov.io/gh/NIAEFEUP/website-niaefeup-backend)
The online platform for NIAEFEUP.

## Development setup
Expand All @@ -21,7 +21,7 @@ For automatic restart to fire up every time a source file changes, make sure tha
Run the following command in your shell:

```bash
gradle bootRun
./gradlew bootRun
```

### Linting
Expand All @@ -35,21 +35,27 @@ Although IntelliJ does not provide linting suggestions for Kotlin out of the box
You can fire up the analysis yourself by running in your shell:

```bash
gradle ktlintCheck
./gradlew ktlintCheck
```

You can fix the lint automatically by running in your shell:

```bash
./gradlew ktlintFormat
```

#### With a git hook

You can setup a local precommit [git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) for lint analysis running a Gradle task provided by the used linting plugin:

```bash
gradle addKtlintCheckGitPreCommitHook
./gradlew addKtlintCheckGitPreCommitHook
```

Or even an auto-format hook, if that is your thing:

```bash
gradle addKtlintFormatGitPreCommitHook
./gradlew addKtlintFormatGitPreCommitHook
```

### Testing
Expand All @@ -63,7 +69,7 @@ Run the test suite as usual, selecting the respective task for running.
Run the following command in your shell:

```bash
gradle test
./gradlew test
```

## Project Details
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pt.up.fe.ni.website.backend.annotations.validation

import jakarta.validation.Constraint
import jakarta.validation.ConstraintValidator
import jakarta.validation.ConstraintValidatorContext
import jakarta.validation.Payload
import kotlin.reflect.KClass
import pt.up.fe.ni.website.backend.model.Role

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [NoDuplicateRolesValidator::class])
@MustBeDocumented
annotation class NoDuplicateRoles(
val message: String = "{no_duplicate_roles.error}",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<Payload>> = []
)

class NoDuplicateRolesValidator : ConstraintValidator<NoDuplicateRoles, List<Role>> {
override fun isValid(value: List<Role>, context: ConstraintValidatorContext?): Boolean {
val names = value.map { it.name }
return names.size == names.toSet().size
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pt.up.fe.ni.website.backend.annotations.validation

import jakarta.validation.Constraint
import jakarta.validation.ConstraintValidator
import jakarta.validation.ConstraintValidatorContext
import jakarta.validation.Payload
import kotlin.reflect.KClass

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [SchoolYearValidator::class])
@MustBeDocumented
annotation class SchoolYear(
val message: String = "{school_year.error}",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<Payload>> = []
)

class SchoolYearValidator : ConstraintValidator<SchoolYear, String> {
private val regex = Regex("\\d{2}-\\d{2}")

override fun isValid(value: String, context: ConstraintValidatorContext?): Boolean {
if (!value.matches(regex)) return false

val years = value.split("-")
if (years.size != 2) return false

val firstYear = years[0].toIntOrNull() ?: return false
val secondYear = years[1].toIntOrNull() ?: return false
return secondYear == firstYear + 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ class AccountController(private val service: AccountService) {
@PostMapping("/changePassword/{id}")
fun changePassword(@PathVariable id: Long, @RequestBody dto: ChangePasswordDto): Map<String, String> {
service.changePassword(id, dto)
return mapOf()
return emptyMap()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pt.up.fe.ni.website.backend.controller

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.exc.InvalidFormatException
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
Expand All @@ -9,6 +10,7 @@ import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageNotReadableException
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
Expand All @@ -26,7 +28,7 @@ data class CustomError(val errors: List<SimpleError>)

@RestController
@RestControllerAdvice
class ErrorController : ErrorController, Logging {
class ErrorController(private val objectMapper: ObjectMapper) : ErrorController, Logging {

@RequestMapping("/**")
@ResponseStatus(HttpStatus.NOT_FOUND)
Expand All @@ -41,7 +43,23 @@ class ErrorController : ErrorController, Logging {
SimpleError(
violation.message,
violation.propertyPath.toString(),
violation.invalidValue
violation.invalidValue.takeIf { it.isSerializable() }
)
)
}
return CustomError(errors)
}

@ExceptionHandler(MethodArgumentNotValidException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun invalidArguments(e: MethodArgumentNotValidException): CustomError {
val errors = mutableListOf<SimpleError>()
e.bindingResult.fieldErrors.forEach { error ->
errors.add(
SimpleError(
error.defaultMessage ?: "invalid",
error.field,
error.rejectedValue?.takeIf { it.isSerializable() }
)
)
}
Expand Down Expand Up @@ -112,4 +130,11 @@ class ErrorController : ErrorController, Logging {
fun wrapSimpleError(msg: String, param: String? = null, value: Any? = null) = CustomError(
mutableListOf(SimpleError(msg, param, value))
)

fun Any.isSerializable() = try {
objectMapper.writeValueAsString(this)
true
} catch (err: Exception) {
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import pt.up.fe.ni.website.backend.dto.entity.EventDto
import pt.up.fe.ni.website.backend.service.EventService
import pt.up.fe.ni.website.backend.service.activity.EventService

@RestController
@RequestMapping("/events")
Expand All @@ -30,7 +30,10 @@ class EventController(private val service: EventService) {
fun createEvent(@RequestBody dto: EventDto) = service.createEvent(dto)

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

@PutMapping("/{id}")
fun updateEventById(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package pt.up.fe.ni.website.backend.controller

import jakarta.validation.Valid
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import pt.up.fe.ni.website.backend.dto.entity.GenerationDto
import pt.up.fe.ni.website.backend.dto.generations.UpdateGenerationDto
import pt.up.fe.ni.website.backend.service.GenerationService

@RestController
@RequestMapping("/generations")
class GenerationController(private val service: GenerationService) {
@GetMapping
fun getAllGenerations() = service.getAllGenerations()

@GetMapping("/{id:\\d+}")
fun getGenerationById(@PathVariable id: Long) = service.getGenerationById(id)

@GetMapping("/{year:\\d{2}-\\d{2}}")
fun getGenerationByYear(@PathVariable year: String) = service.getGenerationByYear(year)

@GetMapping("/latest")
fun getLatestGeneration() = service.getLatestGeneration()

@PostMapping("/new")
fun createNewGeneration(
@RequestBody dto: GenerationDto
) = service.createNewGeneration(dto)

@PatchMapping("/{id:\\d+}")
fun updateGenerationById(
@PathVariable id: Long,
@RequestBody @Valid
dto: UpdateGenerationDto
) = service.updateGenerationById(id, dto)

@PatchMapping("/{year:\\d{2}-\\d{2}}")
fun updateGenerationByYear(
@PathVariable year: String,
@RequestBody @Valid
dto: UpdateGenerationDto
) = service.updateGenerationByYear(year, dto)

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

@DeleteMapping("/{year:\\d{2}-\\d{2}}")
fun deleteGenerationByYear(@PathVariable year: String): Map<String, String> {
service.deleteGenerationByYear(year)
return emptyMap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ class PostController(private val service: PostService) {
) = service.updatePostById(postId, dto)

@DeleteMapping("/{postId}")
fun deletePost(@PathVariable postId: Long) = service.deletePostById(postId)
fun deletePost(@PathVariable postId: Long): Map<String, String> {
service.deletePostById(postId)
return emptyMap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import pt.up.fe.ni.website.backend.dto.entity.ProjectDto
import pt.up.fe.ni.website.backend.service.ProjectService
import pt.up.fe.ni.website.backend.service.activity.ProjectService

@RestController
@RequestMapping("/projects")
Expand All @@ -28,7 +28,10 @@ class ProjectController(private val service: ProjectService) {
fun createNewProject(@RequestBody dto: ProjectDto) = service.createProject(dto)

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

@PutMapping("/{id}")
fun updatePostById(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.dto.entity

import pt.up.fe.ni.website.backend.model.Generation

class GenerationDto(
var schoolYear: String?,
val roles: List<RoleDto>
) : EntityDto<Generation>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pt.up.fe.ni.website.backend.dto.entity

import com.fasterxml.jackson.annotation.JsonProperty
import pt.up.fe.ni.website.backend.model.PerActivityRole

class PerActivityRoleDto(
@JsonProperty(required = true)
val activityId: Long?,

val permissions: List<Int>
) : EntityDto<PerActivityRole>()
15 changes: 15 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/RoleDto.kt
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.JsonProperty
import pt.up.fe.ni.website.backend.model.Role

class RoleDto(
val name: String,
val permissions: List<Int>,

@JsonProperty(required = true)
val isSection: Boolean?,

val accountIds: List<Long> = emptyList(),
val associatedActivities: List<PerActivityRoleDto>
) : EntityDto<Role>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pt.up.fe.ni.website.backend.dto.generations

import com.fasterxml.jackson.annotation.JsonUnwrapped
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.model.Generation

typealias GetGenerationDto = List<GenerationSectionDto>

data class GenerationUserDto(
@JsonUnwrapped
val account: Account,
val roles: List<String>
)

data class GenerationSectionDto(
val section: String,
val accounts: List<GenerationUserDto>
)

fun buildGetGenerationDto(generation: Generation): GetGenerationDto {
val usedAccounts = mutableSetOf<Account>()
val sections = generation.roles
.filter { it.isSection && it.accounts.isNotEmpty() }
.map { role ->
GenerationSectionDto(
section = role.name,
accounts = role.accounts
.filter { !usedAccounts.contains(it) }
.map { account ->
usedAccounts.add(account)
GenerationUserDto(
account,
roles = account.roles
.filter { it.generation == generation && !it.isSection }
.map { it.name }
)
}
)
}
return sections
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.dto.generations

import pt.up.fe.ni.website.backend.annotations.validation.SchoolYear

data class UpdateGenerationDto(
@SchoolYear
val schoolYear: String
)
Loading