diff --git a/komga-webui/src/components/CardSeries.vue b/komga-webui/src/components/CardSeries.vue index 92284bcff..50c83a38e 100644 --- a/komga-webui/src/components/CardSeries.vue +++ b/komga-webui/src/components/CardSeries.vue @@ -17,9 +17,9 @@ - {{ series.name }} + {{ series.metadata.title }} mdi-close - Edit {{ series.name }} + Edit {{ $_.get(series, 'metadata.title') }} Save changes @@ -18,7 +18,7 @@ mdi-pencil - Edit {{ series.name }} + Edit {{ $_.get(series, 'metadata.title') }} @@ -32,13 +32,59 @@
+ + + + + + + + + + + + + + + + + + + + - + + + @@ -86,7 +132,12 @@ export default Vue.extend({ snackText: '', seriesStatus: Object.keys(SeriesStatus).map(x => capitalize(x)), form: { - status: '' + status: '', + statusLock: false, + title: '', + titleLock: false, + titleSort: '', + titleSortLock: false } } }, @@ -116,15 +167,19 @@ export default Vue.extend({ methods: { dialogReset (series: SeriesDto) { this.form.status = capitalize(series.metadata.status) + this.form.statusLock = series.metadata.statusLock + this.form.title = series.metadata.title + this.form.titleLock = series.metadata.titleLock + this.form.titleSort = series.metadata.titleSort + this.form.titleSortLock = series.metadata.titleSortLock }, dialogCancel () { this.$emit('input', false) this.dialogReset(this.series) }, - dialogConfirm () { - this.editSeries() + async dialogConfirm () { + await this.editSeries() this.$emit('input', false) - this.dialogReset(this.series) }, showSnack (message: string) { this.snackText = message @@ -133,10 +188,16 @@ export default Vue.extend({ async editSeries () { try { const metadata = { - status: this.form.status.toUpperCase() + status: this.form.status.toUpperCase(), + statusLock: this.form.statusLock, + title: this.form.title, + titleLock: this.form.titleLock, + titleSort: this.form.titleSort, + titleSortLock: this.form.titleSortLock } as SeriesMetadataUpdateDto - await this.$komgaSeries.updateMetadata(this.series.id, metadata) + const updatedSeries = await this.$komgaSeries.updateMetadata(this.series.id, metadata) + this.$emit('update:series', updatedSeries) } catch (e) { this.showSnack(e.message) } diff --git a/komga-webui/src/components/SearchBox.vue b/komga-webui/src/components/SearchBox.vue index d2a73d374..c24e7d076 100644 --- a/komga-webui/src/components/SearchBox.vue +++ b/komga-webui/src/components/SearchBox.vue @@ -33,7 +33,7 @@ class="ma-1 mr-3" /> - + diff --git a/komga-webui/src/services/komga-series.service.ts b/komga-webui/src/services/komga-series.service.ts index f3227dcc8..b3c73cb74 100644 --- a/komga-webui/src/services/komga-series.service.ts +++ b/komga-webui/src/services/komga-series.service.ts @@ -105,9 +105,9 @@ export default class KomgaSeriesService { } } - async updateMetadata (seriesId: number, metadata: SeriesMetadataUpdateDto) { + async updateMetadata (seriesId: number, metadata: SeriesMetadataUpdateDto): Promise { try { - await this.http.patch(`${API_SERIES}/${seriesId}/metadata`, metadata) + return (await this.http.patch(`${API_SERIES}/${seriesId}/metadata`, metadata)).data } catch (e) { let msg = `An error occurred while trying to update series metadata` if (e.response.data.message) { diff --git a/komga-webui/src/types/komga-series.ts b/komga-webui/src/types/komga-series.ts index 222cba337..75563eba1 100644 --- a/komga-webui/src/types/komga-series.ts +++ b/komga-webui/src/types/komga-series.ts @@ -10,10 +10,20 @@ interface SeriesDto { interface SeriesMetadata { status: string, + statusLock: boolean, created: string, - lastModified: string + lastModified: string, + title: string, + titleLock: boolean, + titleSort: string, + titleSortLock: boolean } interface SeriesMetadataUpdateDto { - status?: string + status?: string, + statusLock?: boolean, + title?: string, + titleLock?: boolean, + titleSort?: string, + titleSortLock?: boolean } diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index d1c4b2eae..550583c02 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -100,12 +100,13 @@ export default mixins(VisibleElements).extend({ pagesState: [] as LoadState[], pageSize: 20, totalElements: null as number | null, - sortOptions: [{ name: 'Name', key: 'name' }, { name: 'Date added', key: 'createdDate' }, { - name: 'Date updated', - key: 'lastModifiedDate' - }] as SortOption[], + sortOptions: [ + { name: 'Name', key: 'metadata.titleSort' }, + { name: 'Date added', key: 'createdDate' }, + { name: 'Date updated', key: 'lastModifiedDate' } + ] as SortOption[], sortActive: {} as SortActive, - sortDefault: { key: 'name', order: 'asc' } as SortActive, + sortDefault: { key: 'metadata.titleSort', order: 'asc' } as SortActive, filterStatus: [] as string[], SeriesStatus, cardWidth: 150, diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index aeef419da..7c5958aa6 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -24,7 +24,7 @@ - {{ series.name }} + {{ series.metadata.title }} @@ -54,7 +54,7 @@ -
{{ series.name }}
+
{{ series.metadata.title }}
@@ -89,7 +89,7 @@ + :series.sync="series"/> @@ -159,11 +159,6 @@ export default mixins(VisibleElements).extend({ if (this.$route.params.index !== index) { this.updateRoute(index) } - }, - dialogEdit (val) { - if (!val) { - this.loadSeries() - } } }, async created () { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt index c88de22e8..a858aeaf8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt @@ -66,7 +66,7 @@ class Series( @OneToOne(optional = false, orphanRemoval = true, cascade = [CascadeType.ALL], fetch = FetchType.LAZY) @JoinColumn(name = "metadata_id", nullable = false) - var metadata: SeriesMetadata = SeriesMetadata() + var metadata: SeriesMetadata = SeriesMetadata(title = name, titleSort = name) init { this.books = books.toList() diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt index a592744d5..b87723df8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt @@ -18,7 +18,13 @@ import javax.persistence.Table class SeriesMetadata( @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) - var status: Status = Status.ONGOING + var status: Status = Status.ONGOING, + + @Column(name = "title", nullable = false) + var title: String, + + @Column(name = "title_sort", nullable = false) + var titleSort: String ) : AuditableEntity() { @Id @@ -26,6 +32,15 @@ class SeriesMetadata( @Column(name = "id", nullable = false, unique = true) val id: Long = 0 + @Column(name = "status_lock", nullable = false) + var statusLock: Boolean = false + + @Column(name = "title_lock", nullable = false) + var titleLock: Boolean = false + + @Column(name = "title_sort_lock", nullable = false) + var titleSortLock: Boolean = false + enum class Status { ENDED, ONGOING, ABANDONED, HIATUS } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt index 352097ad5..afe7291ea 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt @@ -113,16 +113,16 @@ class OpdsController( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam("search") searchTerm: String? ): OpdsFeed { - val sort = Sort.by(Sort.Order.asc("name").ignoreCase()) + val sort = Sort.by(Sort.Order.asc("metadata.titleSort").ignoreCase()) val series = - mutableListOf>().let { specs -> - if (!principal.user.sharedAllLibraries) { - specs.add(Series::library.`in`(principal.user.sharedLibraries)) - } + mutableListOf>().let { specs -> + if (!principal.user.sharedAllLibraries) { + specs.add(Series::library.`in`(principal.user.sharedLibraries)) + } - if (!searchTerm.isNullOrEmpty()) { - specs.add(Series::name.likeLower("%$searchTerm%")) - } + if (!searchTerm.isNullOrEmpty()) { + specs.add(Series::name.likeLower("%$searchTerm%")) + } if (specs.isNotEmpty()) { seriesRepository.findAll(specs.reduce { acc, spec -> acc.and(spec)!! }, sort) @@ -230,14 +230,14 @@ class OpdsController( OpdsLinkFeedNavigation(OpdsLinkRel.SELF, "${ROUTE_BASE}libraries/$id"), linkStart ), - entries = seriesRepository.findByLibraryId(library.id, Sort.by(Sort.Order.asc("name").ignoreCase())).map { it.toOpdsEntry() } + entries = seriesRepository.findByLibraryId(library.id, Sort.by(Sort.Order.asc("metadata.titleSort").ignoreCase())).map { it.toOpdsEntry() } ) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) private fun Series.toOpdsEntry() = OpdsEntryNavigation( - title = name, + title = metadata.title, updated = lastModifiedDate?.atZone(ZoneId.systemDefault()) ?: ZonedDateTime.now(), id = id.toString(), content = "", diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index cb2514fd3..62e87dc9f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -63,7 +63,7 @@ class SeriesController( page.pageNumber, page.pageSize, if (page.sort.isSorted) page.sort - else Sort.by(Sort.Order.asc("name").ignoreCase()) + else Sort.by(Sort.Order.asc("metadata.titleSort").ignoreCase()) ) return mutableListOf>().let { specs -> @@ -226,6 +226,15 @@ class SeriesController( ): SeriesDto = seriesRepository.findByIdOrNull(seriesId)?.let { series -> newMetadata.status?.let { series.metadata.status = newMetadata.status } + newMetadata.statusLock?.let { series.metadata.statusLock = newMetadata.statusLock } + if (!newMetadata.title.isNullOrBlank()) { + series.metadata.title = newMetadata.title + } + newMetadata.titleLock?.let { series.metadata.titleLock = newMetadata.titleLock } + if (!newMetadata.titleSort.isNullOrBlank()) { + series.metadata.titleSort = newMetadata.titleSort + } + newMetadata.titleSortLock?.let { series.metadata.titleSortLock = newMetadata.titleSortLock } seriesRepository.save(series).toDto(includeUrl = true) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt index 1bd36054a..cfe9b0395 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt @@ -21,10 +21,15 @@ data class SeriesDto( data class SeriesMetadataDto( val status: String, + val statusLock: Boolean, @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") val created: LocalDateTime?, @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - val lastModified: LocalDateTime? + val lastModified: LocalDateTime?, + val title: String, + val titleLock: Boolean, + val titleSort: String, + val titleSortLock: Boolean ) fun Series.toDto(includeUrl: Boolean) = SeriesDto( @@ -38,7 +43,12 @@ fun Series.toDto(includeUrl: Boolean) = SeriesDto( booksCount = books.size, metadata = SeriesMetadataDto( status = metadata.status.name, + statusLock = metadata.statusLock, created = metadata.createdDate?.toUTC(), - lastModified = metadata.lastModifiedDate?.toUTC() + lastModified = metadata.lastModifiedDate?.toUTC(), + title = metadata.title, + titleLock = metadata.titleLock, + titleSort = metadata.titleSort, + titleSortLock = metadata.titleSortLock ) ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt index bda8950bb..714eea022 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt @@ -3,5 +3,10 @@ package org.gotson.komga.interfaces.rest.dto import org.gotson.komga.domain.model.SeriesMetadata data class SeriesMetadataUpdateDto( - val status: SeriesMetadata.Status? + val status: SeriesMetadata.Status?, + val statusLock: Boolean?, + val title: String?, + val titleLock: Boolean?, + val titleSort: String?, + val titleSortLock: Boolean? ) diff --git a/komga/src/main/resources/db/migration/V20200131101624__series_metadata_title.sql b/komga/src/main/resources/db/migration/V20200131101624__series_metadata_title.sql new file mode 100644 index 000000000..e5f087948 --- /dev/null +++ b/komga/src/main/resources/db/migration/V20200131101624__series_metadata_title.sql @@ -0,0 +1,18 @@ +alter table series_metadata + add ( + status_lock boolean default false, + title varchar, + title_lock boolean default false, + title_sort varchar, + title_sort_lock boolean default false + ); + +update series_metadata m +set m.title = (select name from series where metadata_id = m.id), + m.title_sort = (select name from series where metadata_id = m.id); + +alter table series_metadata + alter column title set not null; + +alter table series_metadata + alter column title_sort set not null; diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt index ccb99ff2d..e8c9c4b3e 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt @@ -29,9 +29,9 @@ import javax.sql.DataSource @AutoConfigureTestDatabase @AutoConfigureMockMvc(printOnlyOnFailure = false) class SeriesControllerTest( - @Autowired private val seriesRepository: SeriesRepository, - @Autowired private val libraryRepository: LibraryRepository, - @Autowired private val mockMvc: MockMvc + @Autowired private val seriesRepository: SeriesRepository, + @Autowired private val libraryRepository: LibraryRepository, + @Autowired private val mockMvc: MockMvc ) { lateinit var jdbcTemplate: JdbcTemplate @@ -60,14 +60,36 @@ class SeriesControllerTest( seriesRepository.deleteAll() } + @Nested + inner class SeriesSort { + @Test + @WithMockCustomUser + fun `given series with titleSort when requesting via api then series are sorted by titleSort`() { + val alpha = makeSeries("The Alpha").also { + it.metadata.titleSort = "Alpha, The" + it.library = library + } + seriesRepository.save(alpha) + val beta = makeSeries("Beta").also { it.library = library } + seriesRepository.save(beta) + + mockMvc.get("/api/v1/series") + .andExpect { + status { isOk } + jsonPath("$.content[0].metadata.title") { value("The Alpha") } + jsonPath("$.content[1].metadata.title") { value("Beta") } + } + } + } + @Nested inner class BookOrdering { @Test @WithMockCustomUser fun `given books with unordered index when requesting via api then books are ordered`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1"), makeBook("3")) + name = "series", + books = listOf(makeBook("1"), makeBook("3")) ).also { it.library = library } seriesRepository.save(series) @@ -75,20 +97,20 @@ class SeriesControllerTest( seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/books") - .andExpect { - status { isOk } - jsonPath("$.content[0].name") { value("1") } - jsonPath("$.content[1].name") { value("2") } - jsonPath("$.content[2].name") { value("3") } - } + .andExpect { + status { isOk } + jsonPath("$.content[0].name") { value("1") } + jsonPath("$.content[1].name") { value("2") } + jsonPath("$.content[2].name") { value("3") } + } } @Test @WithMockCustomUser fun `given many books with unordered index when requesting via api then books are ordered and paged`() { val series = makeSeries( - name = "series", - books = (1..100 step 2).map { makeBook("$it") } + name = "series", + books = (1..100 step 2).map { makeBook("$it") } ).also { it.library = library } seriesRepository.save(series) @@ -96,24 +118,24 @@ class SeriesControllerTest( seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/books") - .andExpect { - status { isOk } - jsonPath("$.content[0].name") { value("1") } - jsonPath("$.content[1].name") { value("2") } - jsonPath("$.content[2].name") { value("3") } - jsonPath("$.content[3].name") { value("5") } - jsonPath("$.size") { value(20) } - jsonPath("$.first") { value(true) } - jsonPath("$.number") { value(0) } - } + .andExpect { + status { isOk } + jsonPath("$.content[0].name") { value("1") } + jsonPath("$.content[1].name") { value("2") } + jsonPath("$.content[2].name") { value("3") } + jsonPath("$.content[3].name") { value("5") } + jsonPath("$.size") { value(20) } + jsonPath("$.first") { value(true) } + jsonPath("$.number") { value(0) } + } } @Test @WithMockCustomUser fun `given many books in ready state with unordered index when requesting via api then books are ordered and paged`() { val series = makeSeries( - name = "series", - books = (1..100 step 2).map { makeBook("$it") } + name = "series", + books = (1..100 step 2).map { makeBook("$it") } ).also { it.library = library } seriesRepository.save(series) @@ -122,16 +144,16 @@ class SeriesControllerTest( seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/books?mediaStatus=READY") - .andExpect { - status { isOk } - jsonPath("$.content[0].name") { value("1") } - jsonPath("$.content[1].name") { value("2") } - jsonPath("$.content[2].name") { value("3") } - jsonPath("$.content[3].name") { value("5") } - jsonPath("$.size") { value(20) } - jsonPath("$.first") { value(true) } - jsonPath("$.number") { value(0) } - } + .andExpect { + status { isOk } + jsonPath("$.content[0].name") { value("1") } + jsonPath("$.content[1].name") { value("2") } + jsonPath("$.content[2].name") { value("3") } + jsonPath("$.content[3].name") { value("5") } + jsonPath("$.size") { value(20) } + jsonPath("$.first") { value(true) } + jsonPath("$.number") { value(0) } + } } } @@ -141,8 +163,8 @@ class SeriesControllerTest( @WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = [1]) fun `given user with access to a single library when getting series then only gets series from this library`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1")) + name = "series", + books = listOf(makeBook("1")) ).also { it.library = library } seriesRepository.save(series) @@ -150,17 +172,17 @@ class SeriesControllerTest( libraryRepository.save(otherLibrary) val otherSeries = makeSeries( - name = "otherSeries", - books = listOf(makeBook("2")) + name = "otherSeries", + books = listOf(makeBook("2")) ).also { it.library = otherLibrary } seriesRepository.save(otherSeries) mockMvc.get("/api/v1/series") - .andExpect { - status { isOk } - jsonPath("$.content.length()") { value(1) } - jsonPath("$.content[0].name") { value("series") } - } + .andExpect { + status { isOk } + jsonPath("$.content.length()") { value(1) } + jsonPath("$.content[0].name") { value("series") } + } } } @@ -170,39 +192,39 @@ class SeriesControllerTest( @WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = []) fun `given user with no access to any library when getting specific series then returns unauthorized`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1")) + name = "series", + books = listOf(makeBook("1")) ).also { it.library = library } seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}") - .andExpect { status { isUnauthorized } } + .andExpect { status { isUnauthorized } } } @Test @WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = []) fun `given user with no access to any library when getting specific series thumbnail then returns unauthorized`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1")) + name = "series", + books = listOf(makeBook("1")) ).also { it.library = library } seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/thumbnail") - .andExpect { status { isUnauthorized } } + .andExpect { status { isUnauthorized } } } @Test @WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = []) fun `given user with no access to any library when getting specific series books then returns unauthorized`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1")) + name = "series", + books = listOf(makeBook("1")) ).also { it.library = library } seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/books") - .andExpect { status { isUnauthorized } } + .andExpect { status { isUnauthorized } } } } @@ -212,13 +234,13 @@ class SeriesControllerTest( @WithMockCustomUser fun `given book without thumbnail when getting series thumbnail then returns not found`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1")) + name = "series", + books = listOf(makeBook("1")) ).also { it.library = library } seriesRepository.save(series) mockMvc.get("/api/v1/series/${series.id}/thumbnail") - .andExpect { status { isNotFound } } + .andExpect { status { isNotFound } } } } @@ -228,8 +250,8 @@ class SeriesControllerTest( @WithMockCustomUser fun `given regular user when getting series then url is hidden`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1.cbr")) + name = "series", + books = listOf(makeBook("1.cbr")) ).also { it.library = library } seriesRepository.save(series) @@ -239,27 +261,27 @@ class SeriesControllerTest( } mockMvc.get("/api/v1/series") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/latest") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/new") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/${series.id}") - .andExpect { - status { isOk } - jsonPath("$.url") { value("") } - } + .andExpect { + status { isOk } + jsonPath("$.url") { value("") } + } } @Test @WithMockCustomUser(roles = [UserRoles.ADMIN]) fun `given admin user when getting series then url is available`() { val series = makeSeries( - name = "series", - books = listOf(makeBook("1.cbr")) + name = "series", + books = listOf(makeBook("1.cbr")) ).also { it.library = library } seriesRepository.save(series) @@ -270,19 +292,19 @@ class SeriesControllerTest( } mockMvc.get("/api/v1/series") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/latest") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/new") - .andExpect(validation) + .andExpect(validation) mockMvc.get("/api/v1/series/${series.id}") - .andExpect { - status { isOk } - jsonPath("$.url") { value(url) } - } + .andExpect { + status { isOk } + jsonPath("$.url") { value(url) } + } } } }