Skip to content

Commit ab12738

Browse files
committed
feat/qg-268: реализована подгрузка записей на странице журнала
1 parent 492af86 commit ab12738

9 files changed

Lines changed: 204 additions & 44 deletions

File tree

app/src/main/kotlin/pro/qyoga/app/therapist/clients/journal/list/JournalPageController.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ import org.springframework.stereotype.Controller
55
import org.springframework.ui.ModelMap
66
import org.springframework.web.bind.annotation.GetMapping
77
import org.springframework.web.bind.annotation.PathVariable
8+
import org.springframework.web.bind.annotation.RequestParam
89
import org.springframework.web.servlet.ModelAndView
910
import pro.azhidkov.platform.spring.mvc.viewId
1011
import pro.qyoga.app.therapist.clients.ClientPageFragmentModel
1112
import pro.qyoga.app.therapist.clients.ClientPageModel
1213
import pro.qyoga.app.therapist.clients.ClientPageTab
14+
import pro.qyoga.core.clients.journals.dtos.JournalPageRq
1315
import pro.qyoga.core.clients.journals.dtos.JournalPageRq.Companion.firstPage
1416
import pro.qyoga.core.clients.journals.model.JournalEntry
17+
import java.time.LocalDate
1518
import java.util.*
1619

1720

1821
class JournalPageFragmentModel(
19-
val page: Slice<JournalEntry>
22+
val page: Slice<JournalEntry>,
2023
) :
2124
ClientPageFragmentModel,
2225
ModelAndView(
23-
viewId("therapist/clients/client-journal-fragment"), mapOf(
24-
"journal" to page
26+
viewId("therapist/clients/client-journal-fragment", "journal-entries"), mapOf(
27+
"journal" to page,
28+
"lastEntryDate" to page.lastOrNull()?.date
2529
)
2630
) {
2731

@@ -34,6 +38,16 @@ class JournalPageController(
3438
private val getJournalPage: GetJournalPageOp
3539
) {
3640

41+
@GetMapping(JOURNAL_PAGE_PAGE_PATH)
42+
fun handleGetJournalEntriesPageFragment(
43+
@PathVariable clientId: UUID,
44+
@RequestParam after: LocalDate
45+
): ModelAndView {
46+
val firstPageRq = JournalPageRq.page(clientId, after, fetch = listOf(JournalEntry::therapeuticTask))
47+
val (client, journalPage) = getJournalPage(firstPageRq)
48+
return JournalPageFragmentModel(journalPage)
49+
}
50+
3751
@GetMapping(JOURNAL_PAGE_PATH)
3852
fun handleGetJournalFragment(
3953
@PathVariable clientId: UUID
@@ -50,6 +64,12 @@ class JournalPageController(
5064
companion object {
5165

5266
const val JOURNAL_PAGE_PATH = "/therapist/clients/{clientId}/journal"
67+
const val JOURNAL_PAGE_PAGE_PATH = "/therapist/clients/{clientId}/journal/page"
68+
69+
fun nextPageUrl(lastEntry: JournalEntry): String = JOURNAL_PAGE_PAGE_PATH.replace(
70+
"\\{clientId}".toRegex(),
71+
lastEntry.clientRef.id.toString()
72+
) + "?after=${lastEntry.date}"
5373

5474
}
5575

app/src/main/kotlin/pro/qyoga/core/clients/journals/dtos/JournalPageRq.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@ import kotlin.reflect.KProperty1
99
data class JournalPageRq(
1010
val clientId: UUID,
1111
val date: LocalDate? = null,
12-
val pageSize: Int = 10,
12+
val pageSize: Int = DEFAULT_PAGE_SIZE,
1313
val fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()
1414
) {
1515

1616
companion object {
1717

18+
const val DEFAULT_PAGE_SIZE = 10
19+
1820
fun firstPage(clientId: UUID, fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()) =
1921
JournalPageRq(clientId, fetch = fetch)
2022

23+
fun page(clientId: UUID, after: LocalDate, fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()) =
24+
JournalPageRq(clientId, date = after, fetch = fetch)
25+
2126
fun wholeJournal(clientId: UUID, fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()) =
2227
JournalPageRq(clientId, null, Int.MAX_VALUE, fetch)
2328

app/src/main/resources/templates/therapist/clients/client-journal-fragment.html

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,44 +27,55 @@
2727
</div>
2828
</div>
2929

30-
<div class="journalEntry card mb-5" th:each="entry : ${journal.content}">
31-
<div class="row g-0 card-header">
32-
<div class="col-12 col-sm-1 d-flex align-items-end mb-3 mt-1 mb-sm-0 card-title me-3 ms-2">
33-
<h6 class="entryDate mb-0"
34-
th:text="${#temporals.format(entry.date, T(pro.qyoga.l10n.DateFormatsKt).RUSSIAN_DATE_FORMAT_PATTERN)}"></h6>
35-
</div>
36-
<div class="col-12 col-sm-9 d-flex align-items-end mb-sm-0 ms-2 ms-sm-0 card-subtitle text-muted">
37-
<h6 class="entryTherapeuticTask mb-0 text-truncate">
38-
<span class="d-none d-sm-inline">Задача: </span>
39-
<span th:text="${entry.therapeuticTask.entity.name}"></span>
40-
</h6>
41-
</div>
42-
</div>
43-
<div class="card-body">
44-
<div class="row">
45-
<div class="col">
46-
<p class="entryText" style="white-space: pre-line" th:utext="${entry.entryText}"></p>
30+
<div th:fragment="journal-entries">
31+
<div class="journalEntry card mb-5" th:each="entry : ${journal.content}">
32+
<div class="row g-0 card-header">
33+
<div class="col-12 col-sm-1 d-flex align-items-end mb-3 mt-1 mb-sm-0 card-title me-3 ms-2">
34+
<h6 class="entryDate mb-0"
35+
th:text="${#temporals.format(entry.date, T(pro.qyoga.l10n.DateFormatsKt).RUSSIAN_DATE_FORMAT_PATTERN)}"></h6>
36+
</div>
37+
<div class="col-12 col-sm-9 d-flex align-items-end mb-sm-0 ms-2 ms-sm-0 card-subtitle text-muted">
38+
<h6 class="entryTherapeuticTask mb-0 text-truncate">
39+
<span class="d-none d-sm-inline">Задача: </span>
40+
<span th:text="${entry.therapeuticTask.entity.name}"></span>
41+
</h6>
4742
</div>
4843
</div>
44+
<div class="card-body">
45+
<div class="row">
46+
<div class="col">
47+
<p class="entryText" style="white-space: pre-line" th:utext="${entry.entryText}"></p>
48+
</div>
49+
</div>
4950

50-
<div class="row">
51-
<div class="col-12 d-flex justify-content-end">
52-
<a class="editEntryLink btn btn-outline-success me-3"
53-
hx-target=" #tabContent"
54-
th:hx-get="@{/therapist/clients/{clientId}/journal/{entryId}(clientId=${clientId},entryId=${entry.id})}">
55-
<i class="fas fa-edit"></i>
56-
</a>
57-
<a class="deleteEntryLink btn btn-outline-danger"
58-
hx-swap="outerHTML swap:0.2s"
59-
hx-target="closest div.journalEntry"
60-
th:hx-confirm="'Удалить запись за ' + ${entryDate} + '?'"
61-
th:hx-delete="@{/therapist/clients/{clientId}/journal/{entryId}(clientId=${clientId},entryId=${entry.id})}"
62-
th:with="entryDate=${#temporals.format(entry.date, T(pro.qyoga.l10n.DateFormatsKt).RUSSIAN_DATE_FORMAT_PATTERN)}">
63-
<i class="fas fa-trash"></i>
64-
</a>
51+
<div class="row">
52+
<div class="col-12 d-flex justify-content-end">
53+
<a class="editEntryLink btn btn-outline-success me-3"
54+
hx-target=" #tabContent"
55+
th:hx-get="@{/therapist/clients/{clientId}/journal/{entryId}(clientId=${clientId},entryId=${entry.id})}">
56+
<i class="fas fa-edit"></i>
57+
</a>
58+
<a class="deleteEntryLink btn btn-outline-danger"
59+
hx-swap="outerHTML swap:0.2s"
60+
hx-target="closest div.journalEntry"
61+
th:hx-confirm="'Удалить запись за ' + ${entryDate} + '?'"
62+
th:hx-delete="@{/therapist/clients/{clientId}/journal/{entryId}(clientId=${clientId},entryId=${entry.id})}"
63+
th:with="entryDate=${#temporals.format(entry.date, T(pro.qyoga.l10n.DateFormatsKt).RUSSIAN_DATE_FORMAT_PATTERN)}">
64+
<i class="fas fa-trash"></i>
65+
</a>
66+
</div>
6567
</div>
6668
</div>
6769
</div>
70+
71+
<span class="loader"
72+
hx-select=".journalEntry, .loader"
73+
hx-swap="afterend"
74+
hx-trigger="revealed"
75+
style="height: 0"
76+
th:hx-get="@{/therapist/clients/{clientId}/journal/page(clientId=${clientId},after=${lastEntryDate})}"
77+
th:if="${journal.hasNext()}"
78+
></span>
6879
</div>
6980

7081
</div>

app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/journal/JournalPageTest.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@ package pro.qyoga.tests.cases.app.therapist.clients.journal
33
import org.junit.jupiter.api.DisplayName
44
import org.junit.jupiter.api.Test
55
import org.springframework.http.HttpStatus
6+
import pro.qyoga.core.clients.journals.dtos.JournalPageRq
7+
import pro.qyoga.core.clients.journals.model.JournalEntry
68
import pro.qyoga.tests.assertions.shouldBe
79
import pro.qyoga.tests.assertions.shouldBePage
810
import pro.qyoga.tests.clients.TherapistClient
911
import pro.qyoga.tests.fixture.object_mothers.clients.ClientsObjectMother
1012
import pro.qyoga.tests.fixture.object_mothers.clients.ClientsObjectMother.createClientCardDto
1113
import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_ID
1214
import pro.qyoga.tests.fixture.object_mothers.therapists.theTherapistUserDetails
15+
import pro.qyoga.tests.fixture.presets.ClientsFixturePresets
1316
import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseTest
1417
import pro.qyoga.tests.pages.publc.NotFoundErrorPage
18+
import pro.qyoga.tests.pages.therapist.clients.journal.list.ClientJournalEntriesFragment
1519
import pro.qyoga.tests.pages.therapist.clients.journal.list.EmptyClientJournalPage
1620
import pro.qyoga.tests.pages.therapist.clients.journal.list.NonEmptyClientJournalPage
1721

1822
@DisplayName("Страница журнала клиента")
1923
class JournalPageTest : QYogaAppIntegrationBaseTest() {
2024

25+
private val clientsFixturePresets = getBean<ClientsFixturePresets>()
26+
2127
@Test
2228
fun `должна отображать 10 записей, если они есть`() {
2329
// Сетап
@@ -32,7 +38,7 @@ class JournalPageTest : QYogaAppIntegrationBaseTest() {
3238
val document = therapist.clientJournal.getJournalPage(client.id)
3339

3440
// Проверка
35-
document shouldBe NonEmptyClientJournalPage(client.id, firstPageEntries)
41+
document shouldBe NonEmptyClientJournalPage(client.id, firstPageEntries, hasMore = true)
3642
}
3743

3844
@Test
@@ -92,5 +98,46 @@ class JournalPageTest : QYogaAppIntegrationBaseTest() {
9298
document shouldBe EmptyClientJournalPage(client.id)
9399
}
94100

101+
@Test
102+
fun `должна не содержать лоадер следующей страницы, если кол-во записей в БД меньше размера страницы`() {
103+
// Сетап
104+
val (client, journal) = clientsFixturePresets.createAClientWithJournalEntry()
105+
106+
// Действие
107+
val document = theTherapist.clientJournal.getJournalPage(client.id)
108+
109+
// Проверка
110+
document shouldBe NonEmptyClientJournalPage(client.id, journal, hasMore = false)
111+
}
112+
113+
@Test
114+
fun `при запросе второй страницы должна содержать лоадер, если кол-во записей в БД больше размера двух страниц`() {
115+
// Сетап
116+
val (client, journal) = clientsFixturePresets.createAClientWithJournalEntries(journalEntriesCount = JournalPageRq.DEFAULT_PAGE_SIZE * 2 + 1)
117+
val firstPage = journal.sortedByDescending(JournalEntry::date).take(JournalPageRq.DEFAULT_PAGE_SIZE)
118+
val secondPage = journal.sortedByDescending(JournalEntry::date).drop(JournalPageRq.DEFAULT_PAGE_SIZE)
119+
.take(JournalPageRq.DEFAULT_PAGE_SIZE)
120+
121+
// Действие
122+
val document = theTherapist.clientJournal.getJournalPagePage(client.id, firstPage.last().date)
123+
124+
// Проверка
125+
document shouldBe ClientJournalEntriesFragment.fragmentFor(secondPage, hasMore = true)
126+
}
127+
128+
@Test
129+
fun `при запросе второй страницы должна не содержать лоадер, если кол-во записей в БД равно размеру двух страниц`() {
130+
// Сетап
131+
val (client, journal) = clientsFixturePresets.createAClientWithJournalEntries(journalEntriesCount = JournalPageRq.DEFAULT_PAGE_SIZE * 2)
132+
val firstPage = journal.sortedByDescending(JournalEntry::date).take(JournalPageRq.DEFAULT_PAGE_SIZE)
133+
val secondPage = journal.sortedByDescending(JournalEntry::date).drop(JournalPageRq.DEFAULT_PAGE_SIZE)
134+
.take(JournalPageRq.DEFAULT_PAGE_SIZE)
135+
136+
// Действие
137+
val document = theTherapist.clientJournal.getJournalPagePage(client.id, firstPage.last().date)
138+
139+
// Проверка
140+
document shouldBe ClientJournalEntriesFragment.fragmentFor(secondPage, hasMore = false)
141+
}
95142

96143
}

app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistClientJournalApi.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import pro.qyoga.core.clients.journals.dtos.EditJournalEntryRq
1515
import pro.qyoga.tests.pages.therapist.clients.journal.entry.CreateJournalEntryForm
1616
import pro.qyoga.tests.pages.therapist.clients.journal.entry.EditJournalEntryPage
1717
import pro.qyoga.tests.platform.pathToRegex
18+
import java.time.LocalDate
1819
import java.util.*
1920

2021

@@ -74,6 +75,20 @@ class TherapistClientJournalApi(override val authCookie: Cookie) : AuthorizedApi
7475
}
7576
}
7677

78+
fun getJournalPagePage(clientId: UUID, after: LocalDate): Document {
79+
return Given {
80+
authorized()
81+
pathParam("clientId", clientId)
82+
queryParam("after", after.toString())
83+
} When {
84+
get(JournalPageController.JOURNAL_PAGE_PAGE_PATH)
85+
} Then {
86+
statusCode(HttpStatus.OK.value())
87+
} Extract {
88+
Jsoup.parse(body().asString())
89+
}
90+
}
91+
7792
fun createJournalEntry(clientId: UUID, journalEntry: EditJournalEntryRq) {
7893
postNewJournalEntry(journalEntry, clientId) Then {
7994
statusCode(HttpStatus.OK.value())

app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/clients/JournalEntriesObjectMother.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ object JournalEntriesObjectMother {
1414
text: String = randomSentence(1, 100)
1515
) = EditJournalEntryRq(date, therapeuticTaskName, text, 1)
1616

17+
fun journalEntriesWithUniqueDate(): () -> EditJournalEntryRq {
18+
val datesIterator = generateSequence { randomRecentLocalDate() }
19+
.distinct()
20+
.iterator()
21+
22+
return { journalEntry(date = datesIterator.next()) }
23+
}
24+
1725
}

app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/ClientsFixturePresets.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import pro.qyoga.core.users.auth.dtos.QyogaUserDetails
99
import pro.qyoga.tests.fixture.backgrounds.ClientJournalBackgrounds
1010
import pro.qyoga.tests.fixture.backgrounds.ClientsBackgrounds
1111
import pro.qyoga.tests.fixture.object_mothers.clients.ClientsObjectMother
12-
import pro.qyoga.tests.fixture.object_mothers.clients.JournalEntriesObjectMother
12+
import pro.qyoga.tests.fixture.object_mothers.clients.JournalEntriesObjectMother.journalEntriesWithUniqueDate
13+
import pro.qyoga.tests.fixture.object_mothers.clients.JournalEntriesObjectMother.journalEntry
1314
import pro.qyoga.tests.fixture.object_mothers.therapists.theTherapistUserDetails
1415

1516
data class ClientWithJournal(
@@ -26,7 +27,7 @@ class ClientsFixturePresets(
2627
fun createAClientWithJournalEntry(
2728
therapistUserDetails: QyogaUserDetails = theTherapistUserDetails,
2829
createClient: () -> ClientCardDto = { ClientsObjectMother.createClientCardDtoMinimal() },
29-
createEditJournalEntryRequest: () -> EditJournalEntryRq = { JournalEntriesObjectMother.journalEntry() }
30+
createEditJournalEntryRequest: () -> EditJournalEntryRq = { journalEntry() }
3031
): ClientWithJournal {
3132
val client = clientsBackgrounds.createClient(createClient(), therapistUserDetails)
3233
val journalEntry = clientJournalBackgrounds.createJournalEntry(
@@ -38,10 +39,28 @@ class ClientsFixturePresets(
3839
return ClientWithJournal(client, listOf(journalEntry))
3940
}
4041

42+
fun createAClientWithJournalEntries(
43+
therapistUserDetails: QyogaUserDetails = theTherapistUserDetails,
44+
createClient: () -> ClientCardDto = { ClientsObjectMother.createClientCardDtoMinimal() },
45+
createEditJournalEntryRequest: () -> EditJournalEntryRq = journalEntriesWithUniqueDate(),
46+
journalEntriesCount: Int = 2
47+
): ClientWithJournal {
48+
val client = clientsBackgrounds.createClient(createClient(), therapistUserDetails)
49+
val journal = (1..journalEntriesCount).map {
50+
clientJournalBackgrounds.createJournalEntry(
51+
client.id,
52+
createEditJournalEntryRequest(),
53+
therapistUserDetails
54+
)
55+
}
56+
57+
return ClientWithJournal(client, journal)
58+
}
59+
4160
fun createAClientsWithJournalEntry(
4261
therapistUserDetails: QyogaUserDetails = theTherapistUserDetails,
4362
createClient: () -> ClientCardDto = { ClientsObjectMother.createClientCardDtoMinimal() },
44-
createEditJournalEntryRequest: () -> EditJournalEntryRq = { JournalEntriesObjectMother.journalEntry() },
63+
createEditJournalEntryRequest: () -> EditJournalEntryRq = { journalEntry() },
4564
clientsCount: Int = 2
4665
): List<ClientWithJournal> {
4766
return (1..clientsCount).map {

0 commit comments

Comments
 (0)