Ajout du support pour les kits de retouches (pas juste les PDFs)

This commit is contained in:
FyloZ 2021-05-19 23:29:53 -04:00
parent 42adb0ce9b
commit cadf3dde8b
27 changed files with 427 additions and 237 deletions

View File

@ -19,7 +19,6 @@ plugins {
}
repositories {
jcenter()
mavenCentral()
maven {

View File

@ -14,12 +14,6 @@ annotation class PreAuthorizeViewRecipes
@PreAuthorize("hasAuthority('EDIT_RECIPES')")
annotation class PreAuthorizeEditRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_RECIPES')")
annotation class PreAuthorizeRemoveRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@ -37,9 +31,3 @@ annotation class PreAuthorizeViewUsers
@MustBeDocumented
@PreAuthorize("hasAuthority('EDIT_USERS')")
annotation class PreAuthorizeEditUsers
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_USERS')")
annotation class PreAuthorizeRemoveUsers

View File

@ -8,9 +8,6 @@ import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val COMPANY_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val COMPANY_NAME_NULL_MESSAGE = "Un nom est requis"
@Entity
@Table(name = "company")
data class Company(
@ -20,11 +17,15 @@ data class Company(
@Column(unique = true)
override val name: String
) : NamedModel
) : NamedModel {
override fun toString(): String {
return name
}
}
open class CompanySaveDto(
@field:NotBlank(message = COMPANY_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String
) : EntityDto<Company> {
override fun toEntity(): Company = Company(null, name)
@ -32,10 +33,9 @@ open class CompanySaveDto(
open class CompanyUpdateDto(
@field:NotNull(message = COMPANY_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = COMPANY_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?
) : EntityDto<Company> {
override fun toEntity(): Company = Company(id, name ?: "")

View File

@ -4,27 +4,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val MATERIAL_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
private const val MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE = "Un produit est requis"
private const val MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE = "Une quantité est requises"
private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
import javax.validation.constraints.Size
const val SIMDUT_FILES_PATH = "pdf/simdut"
@ -52,32 +36,27 @@ data class Material(
@JsonIgnore
@Transient
get() = "$SIMDUT_FILES_PATH/$name.pdf"
}
open class MaterialSaveDto(
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val simdutFile: MultipartFile? = null
) : EntityDto<Material>
open class MaterialUpdateDto(
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val inventoryQuantity: Float?,
val materialTypeId: Long?,
@ -95,11 +74,9 @@ data class MaterialOutputDto(
) : Model
data class MaterialQuantityDto(
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
val material: Long,
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val quantity: Float
)
@ -147,7 +124,7 @@ fun materialQuantityDto(
) = MaterialQuantityDto(materialId, quantity).apply(op)
// ==== Exceptions ====
private const
private const
val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found"
private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists"
private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material"

View File

@ -11,10 +11,7 @@ import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val MATERIAL_TYPE_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_TYPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_TYPE_PREFIX_NULL_MESSAGE = "Un préfixe est requis"
private const val MATERIAL_TYPE_PREFIX_SIZE_MESSAGE = "Le préfixe doit faire exactement 3 caractères"
private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters"
@Entity
@Table(name = "material_type")
@ -39,11 +36,11 @@ data class MaterialType(
) : NamedModel
open class MaterialTypeSaveDto(
@field:NotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotBlank(message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_SIZE_MESSAGE)
@field:NotBlank
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String,
val usePercentages: Boolean = false
@ -53,13 +50,12 @@ open class MaterialTypeSaveDto(
}
open class MaterialTypeUpdateDto(
@field:NotNull(message = MATERIAL_TYPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrSize(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String?
) : EntityDto<MaterialType> {
override fun toEntity(): MaterialType =

View File

@ -4,20 +4,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val MIX_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MIX_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MIX_RECIPE_NULL_MESSAGE = "Un recette est requise"
private const val MIX_MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MIX_DEDUCT_MIX_ID_NULL_MESSAGE = "Un identifiant de mélange est requis"
private const val MIX_DEDUCT_RATIO_NULL_MESSAGE = "Un ratio est requis"
private const val MIX_DEDUCT_RATION_NEGATIVE_MESSAGE = "Le ratio doit être égal ou supérieur à 0"
@Entity
@Table(name = "mix")
@ -43,33 +33,26 @@ data class Mix(
) : Model
open class MixSaveDto(
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
val recipeId: Long,
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
) : EntityDto<Mix>
open class MixUpdateDto(
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
) : EntityDto<Mix>
data class MixOutputDto(
val id: Long,
@ -79,16 +62,13 @@ data class MixOutputDto(
)
data class MixDeductDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val ratio: Float
)
data class MixLocationDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val mixId: Long,
val location: String?

View File

@ -6,10 +6,6 @@ import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
private const val MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE = "Un identifiant de produit est requis"
private const val MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE = "La quantité ne peut pas être négative"
@Entity
@Table(name = "mix_material")
data class MixMaterial(
@ -26,6 +22,15 @@ data class MixMaterial(
var position: Int
) : Model
data class MixMaterialDto(
val materialId: Long,
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val quantity: Float,
val position: Int
)
data class MixMaterialOutputDto(
val id: Long,
val material: MaterialOutputDto,
@ -33,17 +38,6 @@ data class MixMaterialOutputDto(
val position: Int
)
data class MixMaterialDto(
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
val materialId: Long,
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float,
val position: Int
)
// ==== DSL ====
fun mixMaterial(
id: Long? = null,

View File

@ -15,3 +15,8 @@ interface EntityDto<out E> {
throw UnsupportedOperationException()
}
}
// GENERAL VALIDATION MESSAGES
const val VALIDATION_SIZE_GE_ZERO = "Must be greater or equals to 0"
const val VALIDATION_SIZE_GE_ONE = "Must be greater or equals to 1"
const val VALIDATION_RANGE_PERCENTS = "Must be between 0 and 100"

View File

@ -5,8 +5,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import java.net.URLEncoder
@ -15,19 +13,7 @@ import java.time.LocalDate
import javax.persistence.*
import javax.validation.constraints.*
private const val RECIPE_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val RECIPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val RECIPE_DESCRIPTION_NULL_MESSAGE = "Une description est requise"
private const val RECIPE_COLOR_NULL_MESSAGE = "Une couleur est requise"
private const val RECIPE_GLOSS_NULL_MESSAGE = "Le lustre de la couleur est requis"
private const val RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE = "Le lustre doit être entre 0 et 100"
private const val RECIPE_SAMPLE_TOO_SMALL_MESSAGE = "Le numéro d'échantillon doit être supérieur ou égal à 0"
private const val RECIPE_COMPANY_NULL_MESSAGE = "Une bannière est requise"
private const val RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
private const val RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE = "Des messages sont requis"
private const val NOTE_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
@ -91,30 +77,28 @@ data class Recipe(
}
open class RecipeSaveDto(
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
@field:NotBlank
val description: String,
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
@field:NotBlank
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String,
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
val gloss: Byte,
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?,
val approbationDate: LocalDate?,
val remark: String?,
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
val companyId: Long = -1L,
val companyId: Long
) : EntityDto<Recipe> {
override fun toEntity(): Recipe = recipe(
name = name,
@ -127,24 +111,23 @@ open class RecipeSaveDto(
}
open class RecipeUpdateDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
@field:NotBlank
val description: String?,
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
@field:NotBlank
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String?,
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
val gloss: Byte?,
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?,
val approbationDate: LocalDate?,
@ -188,15 +171,12 @@ data class RecipeGroupInformation(
)
data class RecipeStepsDto(
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
val steps: Set<RecipeStep>
)
data class RecipePublicDataDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val recipeId: Long,
val notes: Set<NoteDto>?,
@ -205,7 +185,6 @@ data class RecipePublicDataDto(
)
data class NoteDto(
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
val content: String?
@ -290,8 +269,6 @@ private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found"
private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists"
private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe"
sealed class RecipeException
fun recipeIdNotFoundException(id: Long) =
NotFoundException(
RECIPE_EXCEPTION_ERROR_CODE,

View File

@ -3,21 +3,16 @@ package dev.fyloz.colorrecipesexplorer.model.account
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.NamedModel
import dev.fyloz.colorrecipesexplorer.model.*
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"
@Entity
@Table(name = "user_group")
data class Group(
@ -43,11 +38,10 @@ data class Group(
}
open class GroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
@field:NotBlank
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
@ -55,14 +49,12 @@ open class GroupSaveDto(
}
open class GroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
@field:NotBlank
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =

View File

@ -9,14 +9,11 @@ enum class Permission(
) {
READ_FILE,
WRITE_FILE(listOf(READ_FILE)),
REMOVE_FILE(listOf(WRITE_FILE)),
VIEW_RECIPES(listOf(READ_FILE)),
VIEW_CATALOG(listOf(READ_FILE)),
VIEW_USERS,
PRINT_MIXES(listOf(VIEW_RECIPES)),
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)),
@ -25,29 +22,24 @@ enum class Permission(
EDIT_USERS(listOf(VIEW_USERS)),
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE)),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE)),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
REMOVE_USERS(listOf(EDIT_USERS)),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
VIEW_TOUCH_UP_KITS,
EDIT_TOUCH_UP_KITS(listOf(VIEW_TOUCH_UP_KITS)),
PRINT_MIXES(listOf(VIEW_RECIPES)),
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
GENERATE_TOUCH_UP_KIT,
ADMIN(
listOf(
EDIT_RECIPES,
EDIT_CATALOG,
EDIT_USERS,
REMOVE_RECIPES,
REMOVE_USERS,
REMOVE_CATALOG,
EDIT_TOUCH_UP_KITS,
PRINT_MIXES,
ADD_TO_INVENTORY,
DEDUCT_FROM_INVENTORY,
GENERATE_TOUCH_UP_KIT
)
),
@ -69,6 +61,16 @@ enum class Permission(
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
REMOVE_FILE(listOf(WRITE_FILE), true),
GENERATE_TOUCH_UP_KIT(listOf(VIEW_TOUCH_UP_KITS), true),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE), true),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE), true),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES), true),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES), true),
REMOVE_USERS(listOf(EDIT_USERS), true),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES), true),
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),

View File

@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
@ -13,14 +12,9 @@ import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val USER_ID_NULL_MESSAGE = "Un numéro d'utilisateur est requis"
private const val USER_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis"
private const val USER_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val USER_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val USER_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters"
@Entity
@Table(name = "user")
@ -70,19 +64,17 @@ data class User(
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
/** DTO for creating users. Allows a [password] a [groupId]. */
open class UserSaveDto(
@field:NotNull(message = USER_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE)
@field:NotBlank
val firstName: String,
@field:NotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE)
@field:NotBlank
val lastName: String,
@field:NotBlank(message = USER_PASSWORD_EMPTY_MESSAGE)
@field:Size(min = 8, message = USER_PASSWORD_TOO_SHORT_MESSAGE)
@field:NotBlank
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
val password: String,
val groupId: Long?,
@ -92,13 +84,12 @@ open class UserSaveDto(
) : EntityDto<User>
open class UserUpdateDto(
@field:NotNull(message = USER_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE)
@field:NotBlank
val firstName: String?,
@field:NullOrNotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE)
@field:NotBlank
val lastName: String?,
val groupId: Long?,

View File

@ -1,12 +1,211 @@
package dev.fyloz.colorrecipesexplorer.model.touchupkit
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE
import java.time.LocalDate
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
const val TOUCH_UP_KIT_DELIMITER = ';'
@Entity
@Table(name = "touch_up_kit")
data class TouchUpKit(
val id: Long,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val project: String,
val buggy: String
val buggy: String,
val company: String,
val quantity: Int,
@Column(name = "shipping_date")
val shippingDate: LocalDate,
@Column(name = "finish")
private val finishConcatenated: String,
@Column(name = "material")
private val materialConcatenated: String,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "touch_up_kit_id")
val content: Set<TouchUpKitProduct>
) : Model {
val finish
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
val material
get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER)
}
@Entity
@Table(name = "touch_up_kit_product")
data class TouchUpKitProduct(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val name: String,
val description: String?,
val quantity: Float
) : Model
data class TouchUpKitSaveDto(
@field:NotBlank
val project: String,
@field:NotBlank
val buggy: String,
@field:NotBlank
val company: String,
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
val quantity: Int,
val shippingDate: LocalDate,
@field:NotEmpty
val finish: List<String>,
@field:NotEmpty
val material: List<String>,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>
) : EntityDto<TouchUpKit> {
override fun toEntity() = touchUpKit(this)
}
data class TouchUpKitUpdateDto(
val id: Long,
@field:NotBlank
val project: String?,
@field:NotBlank
val buggy: String?,
@field:NotBlank
val company: String?,
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
val quantity: Int?,
val shippingDate: LocalDate?,
@field:NotEmpty
val finish: List<String>?,
@field:NotEmpty
val material: List<String>?,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>?
) : EntityDto<TouchUpKit>
data class TouchUpKitOutputDto(
override val id: Long,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
val shippingDate: LocalDate,
val finish: List<String>,
val material: List<String>,
val content: Set<TouchUpKitProduct>,
val pdfUrl: String
) : Model
data class TouchUpKitProductDto(
val name: String,
val description: String?,
val quantity: Float
)
sealed class TouchUpKitCompany {
inline class CompanyName(val name: String)
class Company(val company: Company)
}
// ==== DSL ====
fun touchUpKit(
id: Long? = null,
project: String = "project",
buggy: String = "buggy",
company: String = "company",
quantity: Int = 1,
shippingDate: LocalDate = LocalDate.now(),
finish: List<String>,
material: List<String>,
content: Set<TouchUpKitProduct>,
op: TouchUpKit.() -> Unit = {}
) = TouchUpKit(
id,
project,
buggy,
company,
quantity,
shippingDate,
finish.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
material.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
content
).apply(op)
fun touchUpKit(touchUpKitSaveDto: TouchUpKitSaveDto) =
with(touchUpKitSaveDto) {
touchUpKit(
project = project,
buggy = buggy,
company = company,
quantity = quantity,
shippingDate = shippingDate,
finish = finish,
material = material,
content = content.map { touchUpKitProduct(it) }.toSet()
)
}
fun touchUpKitProduct(
id: Long? = null,
name: String = "product",
description: String? = "description",
quantity: Float = 1f,
op: TouchUpKitProduct.() -> Unit = {}
) = TouchUpKitProduct(id, name, description, quantity)
.apply(op)
fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) =
touchUpKitProduct(
name = touchUpKitProductDto.name,
description = touchUpKitProductDto.description,
quantity = touchUpKitProductDto.quantity
)
// ==== Exceptions ====
private const val TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE = "Touch up kit not found"
private const val TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE = "Touch up kit already exists"
private const val TOUCH_UP_KIT_EXCEPTION_ERROR_CODE = "touchupkit"
fun touchUpKitIdNotFoundException(id: Long) =
NotFoundException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE,
"A touch up kit with the id $id could not be found",
id
)
fun touchUpKitIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE,
"A touch up kit with the id $id already exists",
id
)

View File

@ -0,0 +1,6 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
import org.springframework.data.jpa.repository.JpaRepository
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long>

View File

@ -1,7 +1,6 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.service.UserService
@ -87,7 +86,7 @@ class UserController(private val userService: UserService) {
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
@PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) =
userService.deleteById(id)
}
@ -147,7 +146,7 @@ class GroupsController(
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
@PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) =
noContent {
groupService.deleteById(id)

View File

@ -38,7 +38,7 @@ class CompanyController(private val companyService: CompanyService) {
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_COMPANIES')")
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun deleteById(@PathVariable id: Long) =
noContent {
companyService.deleteById(id)

View File

@ -64,7 +64,7 @@ class MaterialController(
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')")
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialService.deleteById(id)

View File

@ -38,7 +38,7 @@ class MaterialTypeController(private val materialTypeService: MaterialTypeServic
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPES')")
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialTypeService.deleteById(id)

View File

@ -1,11 +1,8 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.service.MixService
import dev.fyloz.colorrecipesexplorer.service.RecipeImageService
import dev.fyloz.colorrecipesexplorer.service.RecipeService
@ -14,8 +11,6 @@ import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.validation.Valid
@ -61,7 +56,7 @@ class RecipeController(
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
recipeService.deleteById(id)
@ -105,7 +100,7 @@ class MixController(private val mixService: MixService) {
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixService.deleteById(id)

View File

@ -0,0 +1,63 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit"
@RestController
@RequestMapping(TOUCH_UP_KIT_CONTROLLER_PATH)
@PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService
) {
@GetMapping
fun getAll() =
ok(touchUpKitService.getAllForOutput())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(touchUpKitService.getByIdForOutput(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) =
created<TouchUpKitOutputDto>(TOUCH_UP_KIT_CONTROLLER_PATH) {
with(touchUpKitService) {
save(touchUpKit).toOutput()
}
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) =
noContent {
touchUpKitService.update(touchUpKit)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun deleteById(@PathVariable id: Long) =
noContent {
touchUpKitService.deleteById(id)
}
@GetMapping("pdf")
fun getJobPdf(@RequestParam project: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(project)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$project.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -46,7 +46,7 @@ class FileController(
}
@DeleteMapping
@PreAuthorize("hasAnyAuthority('REMOVE_FILE')")
@PreAuthorize("hasAnyAuthority('WRITE_FILE')")
fun delete(@RequestParam path: String): ResponseEntity<Void> {
return noContent {
fileService.delete(path)

View File

@ -1,26 +0,0 @@
package dev.fyloz.colorrecipesexplorer.rest.files
import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/touchup")
@PreAuthorize("hasAuthority('GENERATE_TOUCH_UP_KIT')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService
) {
@GetMapping
fun getJobPdf(@RequestParam job: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(job)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$job.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -136,7 +136,7 @@ class MaterialServiceImpl(
override fun delete(entity: Material) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity)
fileService.delete(entity.simdutFilePath)
if (fileService.exists(entity.simdutFilePath)) fileService.delete(entity.simdutFilePath)
super.delete(entity)
}
}

View File

@ -1,6 +1,12 @@
package dev.fyloz.colorrecipesexplorer.service.files
package dev.fyloz.colorrecipesexplorer.service.touchupkit
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.touchupkit.*
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.service.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import dev.fyloz.colorrecipesexplorer.utils.*
import org.springframework.core.io.ByteArrayResource
import org.springframework.stereotype.Service
@ -10,7 +16,8 @@ private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
interface TouchUpKitService {
interface TouchUpKitService :
ExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository> {
/** Generates and returns a [PdfDocument] for the given [job]. */
fun generateJobPdf(job: String): PdfDocument
@ -29,8 +36,45 @@ interface TouchUpKitService {
@Service
class TouchUpKitServiceImpl(
private val fileService: FileService,
private val creProperties: CreProperties
) : TouchUpKitService {
touchUpKitRepository: TouchUpKitRepository,
private val creProperties: CreProperties,
) : AbstractExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>(
touchUpKitRepository
), TouchUpKitService {
override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id)
override fun TouchUpKit.toOutput() = TouchUpKitOutputDto(
this.id!!,
this.project,
this.buggy,
this.company,
this.quantity,
this.shippingDate,
this.finish,
this.material,
this.content,
this.pdfUrl()
)
override fun update(entity: TouchUpKitUpdateDto): TouchUpKit {
val persistedKit by lazy { getById(entity.id) }
return super.update(with(entity) {
touchUpKit(
id = id,
project = project ?: persistedKit.project,
buggy = buggy ?: persistedKit.buggy,
company = company ?: persistedKit.company,
quantity = quantity ?: persistedKit.quantity,
shippingDate = shippingDate ?: persistedKit.shippingDate,
finish = finish ?: persistedKit.finish,
material = material ?: persistedKit.material,
content = content?.map { touchUpKitProduct(it) }?.toSet() ?: persistedKit.content
)
})
}
override fun generateJobPdf(job: String) = pdf {
container {
centeredVertically = true
@ -75,4 +119,7 @@ class TouchUpKitServiceImpl(
private fun String.pdfDocumentPath() =
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
private fun TouchUpKit.pdfUrl() =
"${creProperties.deploymentUrl}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project"
}

View File

@ -21,7 +21,7 @@ entities.material-types.baseName=Base
databaseupdater.username=root
databaseupdater.password=pass
# DEBUG
spring.jpa.show-sql=true
spring.jpa.show-sql=false
# Do not modify
spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB
@ -30,4 +30,6 @@ spring.jpa.open-in-view=true
server.http2.enabled=true
server.error.whitelabel.enabled=false
spring.h2.console.enabled=false
spring.jackson.deserialization.fail-on-null-for-primitives=true
spring.jackson.default-property-inclusion=non_null
spring.profiles.active=@spring.profiles.active@

View File

@ -1 +0,0 @@
junit.jupiter.testinstance.lifecycle.default=per_class

View File

@ -1,6 +1,10 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_EN
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_FR
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitServiceImpl
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.*
@ -10,13 +14,14 @@ import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals
private class TouchUpKitServiceTestContext {
val touchUpKitRepository = mockk<TouchUpKitRepository>()
val fileService = mockk<FileService> {
every { write(any<ByteArrayResource>(), any(), any()) } just Runs
}
val creProperties = mockk<CreProperties> {
every { cacheGeneratedFiles } returns false
}
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties))
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, touchUpKitRepository, creProperties))
val pdfDocumentData = mockk<ByteArrayResource>()
val pdfDocument = mockk<PdfDocument> {
mockkStatic(PdfDocument::toByteArrayResource)