Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ data class DatabasePlayerProfileEdit(
val profileText: String?,
val sources: List<DatabasePlayerProfileSource>,
val editComment: String?,
val enabled: Boolean
val enabled: Boolean,
val versionTime: Long? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ import io.elephantchess.servicelayer.dto.database.*
import io.elephantchess.utils.cropToFirstNWords
import io.elephantchess.utils.formatWithChineseName
import io.github.reactivecircus.cache4k.Cache
import java.time.Instant
import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatter
import java.util.Locale
import io.ktor.http.*
import kotlinx.html.b
import kotlinx.html.a
import kotlinx.html.li
import kotlinx.html.meta
import kotlinx.html.p
import kotlinx.html.span
import kotlinx.html.style
import kotlinx.html.unsafe
import kotlin.time.Duration.Companion.hours
Expand All @@ -41,7 +47,15 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) {
hasOpeningData: Boolean,
): String {
val description = edit.profileText
val editors = fetchEditorsUsername()
val contributors = fetchEditorsUsername()
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
val contributorsText = contributors.joinToString(", ")
val lastEditDate = edit.versionTime
?.let { Instant.ofEpochMilli(it).atZone(UTC).toLocalDate() }
?.format(DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH))

val playerNameEncodedResolver = SimpleValueTagResolver("player_name_encoded", databasePlayer.urlName)
val playerIdResolver = SimpleValueTagResolver("player_id", databasePlayer.id)
Expand Down Expand Up @@ -72,11 +86,38 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) {
}

val authorMeta = KtorHtmlBuilderTagResolver("author_meta") {
if (editors.isNotEmpty()) {
val names = editors.sortedBy { it.lowercase() }.joinToString(", ")
if (contributors.isNotEmpty()) {
meta {
name = "author"
content = names
content = contributorsText
}
}
}

val profileMetaResolver = KtorHtmlBuilderTagResolver("profile_meta_info") {
if (contributors.isNotEmpty()) {
span {
b { +"Contributors:" }
+" "
contributors.forEachIndexed { index, username ->
if (index > 0) {
+", "
}
a(href = "/@/${username.encodeURLPath()}") {
+username
}
}
}
}
if (contributors.isNotEmpty() && lastEditDate != null) {
span(classes = "profile-meta-separator") {
+" · "
}
}
if (lastEditDate != null) {
span {
b { +"Last edit:" }
+" $lastEditDate"
}
}
}
Expand Down Expand Up @@ -112,6 +153,7 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) {
sourcesResolver,
styleResolver,
authorMeta,
profileMetaResolver,
noIndexMeta,
openingsHeadResolver,
openingsSectionResolver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
margin-bottom: 12px;
}

#profile-meta-info {
margin-bottom: 15px;
font-size: 13px;
}

#player-profile-description .profile-sources {
margin-top: 20px;
padding: 15px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ <h3>Sources</h3>
<ul>{{player_profile_sources}}</ul>
</div>
</div>
<div id="profile-meta-info">{{profile_meta_info}}</div>
<div id="profile-actions">
<a id="view-current-version-link" href="/database/player/{{player_name_encoded}}">view current version</a>
<a href="/database/player/{{player_name_encoded}}/edit-history">view edit history</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.elephantchess.webapp.rendering

import io.elephantchess.htmlrenderer.HtmlRenderer
import io.elephantchess.htmlrenderer.TemplateRenderer
import io.elephantchess.servicelayer.dto.database.DatabasePlayer
import io.elephantchess.servicelayer.dto.database.DatabasePlayerProfileEdit
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class DatabasePageRendererTest {

private fun pageRenderer(): DatabasePageRenderer {
val templateRenderer = object : TemplateRenderer(baseTagResolvers = emptyList(), disabledTemplates = emptyList()) {}
val htmlRenderer = HtmlRenderer(isMinificationEnabled = false, cdnFolder = null, webTemplateRenderer = templateRenderer)
return DatabasePageRenderer(htmlRenderer)
}

@Test
fun `renderPlayerPage includes contributors and last edit date`() = runTest {
val output = pageRenderer().renderPlayerPage(
databasePlayer = DatabasePlayer("player-id", "John Doe", null, null),
requestedVersion = null,
edit = DatabasePlayerProfileEdit(
playerId = "player-id",
canonicalName = "John Doe",
chineseName = null,
gender = null,
profileText = "some text",
sources = emptyList(),
editComment = null,
enabled = true,
versionTime = 1704067200000
),
fetchEditorsUsername = { listOf("Bob", "Alice", "Bob", "<script>") },
hasOpeningData = false
)

assertTrue(output.contains("<b>Contributors:</b>"))
assertTrue(output.contains("&lt;script&gt;"))
assertTrue(output.contains("Alice"))
assertTrue(output.contains("Bob"))
assertTrue(output.contains("""href="/@/Alice""""))
assertTrue(output.contains("""href="/@/Bob""""))
assertTrue(output.contains("""href="/@/%3Cscript%3E""""))
assertTrue(output.contains("<b>Last edit:</b> 1 Jan 2024"))
assertTrue(output.contains("""<meta name="author""""))
val descriptionIndex = output.indexOf("""<div id="player-profile-description">""")
val profileMetaIndex = output.indexOf("""<div id="profile-meta-info">""")
assertTrue(profileMetaIndex > descriptionIndex)
assertFalse(output.contains("<script>"))
}

@Test
fun `renderPlayerPage omits profile metadata when unavailable`() = runTest {
val output = pageRenderer().renderPlayerPage(
databasePlayer = DatabasePlayer("player-id", "John Doe", null, null),
requestedVersion = null,
edit = DatabasePlayerProfileEdit(
playerId = "player-id",
canonicalName = "John Doe",
chineseName = null,
gender = null,
profileText = "some text",
sources = emptyList(),
editComment = null,
enabled = true,
versionTime = null
),
fetchEditorsUsername = { emptyList() },
hasOpeningData = false
)

assertFalse(output.contains("<b>Contributors:</b>"))
assertFalse(output.contains("<b>Last edit:</b>"))
}
}