# HG changeset patch # User Mike Becker # Date 1747496388 -7200 # Node ID 0a9065936aacaa19b77d6063b7ee99d578a6e8cd # Parent b351e70ab325d630d191bb015eeaae364380c159 add "what's new" popup - resolves #670 diff -r b351e70ab325 -r 0a9065936aac build.gradle.kts --- a/build.gradle.kts Fri Mar 14 08:09:05 2025 +0100 +++ b/build.gradle.kts Sat May 17 17:39:48 2025 +0200 @@ -5,7 +5,7 @@ war } group = "de.uapcore" -version = "1.5.1" +version = "1.6.0" repositories { mavenCentral() diff -r b351e70ab325 -r 0a9065936aac setup/postgres/psql_create_tables.sql --- a/setup/postgres/psql_create_tables.sql Fri Mar 14 08:09:05 2025 +0100 +++ b/setup/postgres/psql_create_tables.sql Sat May 17 17:39:48 2025 +0200 @@ -1,10 +1,11 @@ create table lpit_user ( - userid serial primary key, - username text not null unique, - mail text, - lastname text, - givenname text + userid serial primary key, + username text not null unique, + mail text, + lastname text, + givenname text, + knows_updates_until timestamp with time zone ); create type vcstype as enum ('None', 'Mercurial', 'Git'); diff -r b351e70ab325 -r 0a9065936aac setup/postgres/psql_patch_1.5.0.sql --- a/setup/postgres/psql_patch_1.5.0.sql Fri Mar 14 08:09:05 2025 +0100 +++ b/setup/postgres/psql_patch_1.5.0.sql Sat May 17 17:39:48 2025 +0200 @@ -1,4 +1,4 @@ --- apply this script to patch a version < 1.5.0 database to version 1.5.0 +-- apply this script to patch a version 1.1.0 database to version 1.5.0 alter table lpit_issue_history_event add userid integer null references lpit_user (userid) on delete set null; diff -r b351e70ab325 -r 0a9065936aac setup/postgres/psql_patch_1.6.0.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup/postgres/psql_patch_1.6.0.sql Sat May 17 17:39:48 2025 +0200 @@ -0,0 +1,3 @@ +-- apply this script to patch a version 1.5.0 database to version 1.6.0 + +alter table lpit_user add knows_updates_until timestamp with time zone; diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt Sat May 17 17:39:48 2025 +0200 @@ -28,6 +28,7 @@ import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.dao.createDataAccessObject +import de.uapcore.lightpit.entities.User import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -35,6 +36,7 @@ import java.sql.SQLException import java.time.ZoneId import java.util.* +import java.sql.Date as SqlDate abstract class AbstractServlet : HttpServlet() { @@ -89,8 +91,10 @@ ) { val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) val method = mapping.second + val authenticatedUser = req.remoteUser?.let(dao::findUserByName) + showWhatsNewPopup(authenticatedUser, req, dao) logger.trace("invoke {0}", method) - method(HttpRequest(req, resp, params), dao) + method(HttpRequest(authenticatedUser, req, resp, params), dao) } private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" @@ -124,9 +128,7 @@ req.characterEncoding = "UTF-8" // set some internal request attributes - val http = HttpRequest(req, resp) val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") - req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) req.getHeader("Referer")?.let { // TODO: add a sanity check to avoid link injection @@ -136,7 +138,7 @@ // choose the requested language as session language (if available) if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { // language selection stored in cookie - val cookieLocale = cookieLanguage(http) + val cookieLocale = cookieLanguage(req) // if no cookie, fall back to request locale a.k.a "Browser Language" val reqLocale = cookieLocale ?: req.locale @@ -145,7 +147,7 @@ val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() // select the language (this will also refresh the cookie max-age) - selectLanguage(http, sessionLocale) + selectLanguage(req, resp, sessionLocale) logger.debug( "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage @@ -159,17 +161,18 @@ // determine the timezone if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) { // timezone selection stored in cookie - val cookieTimezone = cookieTimezone(http) + val cookieTimezone = cookieTimezone(req) // if no cookie, fall back to server's timezone (the browser does not transmit one) val timezone = cookieTimezone ?: ZoneId.systemDefault() - selectTimezone(http, timezone) + selectTimezone(req, resp, timezone) logger.debug("Timezone for session {0} set to {1}", session.id, timezone) } // if this is an error path, bypass the normal flow if (fullPath.startsWith("/error/")) { + val http = HttpRequest(null, req, resp) http.styleSheets = listOf("error") http.render("error") return @@ -210,6 +213,16 @@ } } + private fun showWhatsNewPopup(user: User?, req: HttpServletRequest, dao: DataAccessObject) { + if (user == null) return + logger.trace("show user with ID {0} what's new", user.id) + val userKnowsUpdatesUntil = dao.untilWhenUserKnowsUpdates(user) + if (userKnowsUpdatesUntil == null || userKnowsUpdatesUntil.before(SqlDate.valueOf(Constants.VERSION_DATE))) { + dao.updateUserKnowsUpdates(user) + req.setAttribute("showWhatsNew", true) + } + } + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { doProcess(req, resp, getMappings) } @@ -224,40 +237,48 @@ return locales.ifEmpty { listOf(Locale.ENGLISH) } } - private fun cookieLanguage(http: HttpRequest): Locale? = - http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } + private fun cookieLanguage(request: HttpServletRequest): Locale? = + request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale - private fun cookieTimezone(http: HttpRequest): ZoneId? = - http.request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } + private fun cookieTimezone(request: HttpServletRequest): ZoneId? = + request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } ?.runCatching { ZoneId.of(this.value)}?.getOrNull() protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) { - http.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) + selectTimezone(http.request, http.response, zoneId) + } + + private fun selectTimezone(request: HttpServletRequest, response: HttpServletResponse, zoneId: ZoneId) { + request.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id) cookie.isHttpOnly = true - cookie.path = http.request.contextPath + cookie.path = request.contextPath cookie.maxAge = COOKIE_MAX_AGE - http.response.addCookie(cookie) + response.addCookie(cookie) } protected fun selectLanguage(http: HttpRequest, locale: Locale) { - http.response.locale = locale - http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) + selectLanguage(http.request, http.response, locale) + } + + private fun selectLanguage(request: HttpServletRequest, response: HttpServletResponse, locale: Locale) { + response.locale = locale + request.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) // delete cookie if language selection matches request locale, otherwise set cookie val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") cookie.isHttpOnly = true - cookie.path = http.request.contextPath - if (http.request.locale.language == locale.language) { + cookie.path = request.contextPath + if (request.locale.language == locale.language) { cookie.maxAge = 0 } else { cookie.value = locale.language cookie.maxAge = COOKIE_MAX_AGE } - http.response.addCookie(cookie) + response.addCookie(cookie) } } diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/Constants.kt --- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt Sat May 17 17:39:48 2025 +0200 @@ -26,6 +26,8 @@ package de.uapcore.lightpit object Constants { + const val VERSION_DATE = "2025-05-17" + /** * The path where the JSP files reside. */ diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Sat May 17 17:39:48 2025 +0200 @@ -27,6 +27,7 @@ import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.entities.HasNode +import de.uapcore.lightpit.entities.User import de.uapcore.lightpit.viewmodel.NavMenu import de.uapcore.lightpit.viewmodel.View import jakarta.servlet.http.HttpServletRequest @@ -52,14 +53,13 @@ class ValidatedValue(val result: T): ValidationResult class HttpRequest( + val user: User?, val request: HttpServletRequest, val response: HttpServletResponse, val pathParams: PathParameters = emptyMap() ) { val session: HttpSession = request.session - val remoteUser: String? = request.remoteUser - /** * The name of the content page. * @@ -153,6 +153,10 @@ */ val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/" + init { + request.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref) + } + private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext) private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp") diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat May 17 17:39:48 2025 +0200 @@ -31,6 +31,7 @@ import de.uapcore.lightpit.viewmodel.IssueSummary import de.uapcore.lightpit.viewmodel.VariantSummary import de.uapcore.lightpit.viewmodel.VersionSummary +import java.sql.Date interface DataAccessObject { @@ -39,6 +40,8 @@ fun findUserByName(username: String): User? fun insertUser(user: User) fun updateUser(user: User) + fun untilWhenUserKnowsUpdates(user: User): Date? + fun updateUserKnowsUpdates(user: User) /** * Lists all versions of the specified [project]. diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat May 17 17:39:48 2025 +0200 @@ -33,6 +33,7 @@ import de.uapcore.lightpit.viewmodel.VersionSummary import org.intellij.lang.annotations.Language import java.sql.Connection +import java.sql.Date import java.sql.PreparedStatement import java.sql.ResultSet @@ -129,6 +130,21 @@ executeUpdate() } } + + override fun untilWhenUserKnowsUpdates(user: User): Date? = + withStatement("select knows_updates_until from lpit_user where userid = ?") { + setInt(1, user.id) + querySingle { it.getDate(1) } + } + + override fun updateUserKnowsUpdates(user: User) { + withStatement("update lpit_user set knows_updates_until = now() where userid = ?") { + with(user) { + setInt(1, id) + } + executeUpdate() + } + } // // diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt --- a/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Sat May 17 17:39:48 2025 +0200 @@ -82,19 +82,18 @@ } fun processIssueForm(issue: Issue, reference: Issue, http: HttpRequest, dao: DataAccessObject) { - val remoteUser = http.remoteUser?.let { dao.findUserByName(it) } if (issue.hasChanged(reference)) { dao.updateIssue(issue) - dao.insertHistoryEvent(remoteUser, issue) + dao.insertHistoryEvent(http.user, issue) } val newComment = http.param("comment") if (!newComment.isNullOrBlank()) { val comment = IssueComment(-1, issue.id).apply { - author = remoteUser + author = http.user comment = newComment } val commentid = dao.insertComment(comment) - dao.insertHistoryEvent(remoteUser, issue, comment, commentid) + dao.insertHistoryEvent(http.user, issue, comment, commentid) } } @@ -110,7 +109,6 @@ } fun processIssueComment(issue:Issue, http: HttpRequest, dao: DataAccessObject): Boolean { - val remoteUser = http.remoteUser?.let { dao.findUserByName(it) } val commentId = http.param("commentid")?.toIntOrNull() ?: -1 if (commentId > 0) { val comment = dao.findComment(commentId) @@ -118,12 +116,12 @@ http.response.sendError(404) return false } - if (comment.author != null && comment.author?.id == remoteUser?.id) { + if (comment.author != null && comment.author?.id == http.user?.id) { val newComment = http.param("comment") if (!newComment.isNullOrBlank()) { comment.comment = newComment dao.updateComment(comment) - dao.insertHistoryEvent(remoteUser, issue, comment) + dao.insertHistoryEvent(http.user, issue, comment) } } else { http.response.sendError(403) @@ -131,11 +129,11 @@ } } else { val comment = IssueComment(-1, issue.id).apply { - author = remoteUser + author = http.user comment = http.param("comment") ?: "" } val newId = dao.insertComment(comment) - dao.insertHistoryEvent(remoteUser, issue, comment, newId) + dao.insertHistoryEvent(http.user, issue, comment, newId) } return true } diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Sat May 17 17:39:48 2025 +0200 @@ -32,7 +32,7 @@ val issues = dao.listIssues(filter.includeDone) .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) - .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "")) + .filter(issueFilterFunction(filter, relationsMap, http.user?.username ?: "")) with(http) { pageTitle = i18n("issues") diff -r b351e70ab325 -r 0a9065936aac src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sat May 17 17:39:48 2025 +0200 @@ -125,7 +125,7 @@ specificVariant, variant ) .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) - .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "")) + .filter(issueFilterFunction(filter, relationsMap, http.user?.username ?: "")) with(http) { pageTitle = project.name @@ -461,9 +461,8 @@ ).applyFormData(http, dao, projectInfo.versions, projectInfo.variants) val openId = if (issue.id < 0) { - val remoteUser = http.remoteUser?.let { dao.findUserByName(it) } val id = dao.insertIssue(issue) - dao.insertHistoryEvent(remoteUser, issue, id) + dao.insertHistoryEvent(http.user, issue, id) id } else { val reference = dao.findIssue(issue.id) diff -r b351e70ab325 -r 0a9065936aac src/main/resources/localization/strings.properties --- a/src/main/resources/localization/strings.properties Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/resources/localization/strings.properties Sat May 17 17:39:48 2025 +0200 @@ -32,6 +32,7 @@ button.comment=Comment button.component.create=New Component button.component.edit=Edit Component +button.dismiss=Dismiss button.issue.all=All Issues button.issue.create.another=Create another Issue button.issue.create=New Issue @@ -45,6 +46,7 @@ button.variant.create=New Variant button.version.create=New Version button.version.edit=Edit Version +button.whats-new=Show Changelog commit.redirect-link=If redirection does not work, click the following link: commit.success=Operation successful - you will be redirected in a second. component.active=Active @@ -193,6 +195,7 @@ version.status.Unreleased=Unreleased version.status=Status version=Version +whats-new.info = A new version of LightPIT has been released. Do you want to check the release notes? issue.relations.type.DefectOf=defect of issue.relations.type.DefectOf.rev=defect issue.filter.sort.primary=Order by diff -r b351e70ab325 -r 0a9065936aac src/main/resources/localization/strings_de.properties --- a/src/main/resources/localization/strings_de.properties Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/resources/localization/strings_de.properties Sat May 17 17:39:48 2025 +0200 @@ -32,6 +32,7 @@ button.comment=Kommentieren button.component.create=Neue Komponente button.component.edit=Komponente Bearbeiten +button.dismiss=Nicht Jetzt button.issue.all=Alle Vorg\u00e4nge button.issue.create.another=Weiteren Vorgang erstellen button.issue.create=Neuer Vorgang @@ -45,6 +46,7 @@ button.variant.create=Neue Variante button.version.create=Neue Version button.version.edit=Version Bearbeiten +button.whats-new=Versionshinweise \u00d6ffnen commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier: commit.success=Operation erfolgreich - Sie werden jeden Moment weitergeleitet. component.active=Aktiv @@ -193,6 +195,7 @@ version.status.Unreleased=Unver\u00f6ffentlicht version.status=Status version=Version +whats-new.info = Eine neue LightPIT-Version wurde ver\u00f6ffentlicht. Wollen Sie mehr erfahren? issue.relations.type.DefectOf.rev=Fehler issue.relations.type.DefectOf=Fehler von issue.filter.sort.primary=Sortiere nach diff -r b351e70ab325 -r 0a9065936aac src/main/webapp/WEB-INF/changelogs/changelog-de.jspf --- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Sat May 17 17:39:48 2025 +0200 @@ -24,6 +24,12 @@ --%> <%@ page contentType="text/html;charset=UTF-8" %> +

Version 1.6.0 (Vorschau)

+ +
    +
  • Pop-Up hinzugefügt, das über eine neue LightPIT-Version informiert.
  • +
+

Version 1.5.1

    diff -r b351e70ab325 -r 0a9065936aac src/main/webapp/WEB-INF/changelogs/changelog.jspf --- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf Sat May 17 17:39:48 2025 +0200 @@ -24,6 +24,12 @@ --%> <%@ page contentType="text/html;charset=UTF-8" %> +

    Version 1.6.0 (preview)

    + +
      +
    • Add popup informing about a new LightPIT release.
    • +
    +

    Version 1.5.1

      diff -r b351e70ab325 -r 0a9065936aac src/main/webapp/WEB-INF/jsp/site.jsp --- a/src/main/webapp/WEB-INF/jsp/site.jsp Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Sat May 17 17:39:48 2025 +0200 @@ -31,7 +31,7 @@ <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%-- Version suffix for forcing browsers to update the CSS / JS files --%> - + <%-- Make the base href easily available at request scope --%> @@ -87,6 +87,16 @@ @@ -94,7 +104,7 @@ -
      +
      class="blurred" >
      + +
      +
      + +
      +
      + + +
      +
      +
      diff -r b351e70ab325 -r 0a9065936aac src/main/webapp/lightpit.css --- a/src/main/webapp/lightpit.css Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/webapp/lightpit.css Sat May 17 17:39:48 2025 +0200 @@ -156,6 +156,21 @@ padding: 1.5em; } +#whats-new { + background: ghostwhite; + position: fixed; + top: 10%; + left: 50%; + transform: translate(-50%, 0); + padding: 1.5em; + border: #1c204e solid 2px; + border-radius: 4px; +} + +.blurred { + filter: blur(5px); +} + button, a.button { display: inline-block; font-size: medium; @@ -277,6 +292,10 @@ margin-top: .5em; } +.bigskip { + margin-top: 1.5em; +} + .info-box, .error-box, .warn-box { margin: 1.5em; border-style: dashed;