diff --git a/BudgetMasterServer/pom.xml b/BudgetMasterServer/pom.xml index f7e5fcec9..2ee143d64 100644 --- a/BudgetMasterServer/pom.xml +++ b/BudgetMasterServer/pom.xml @@ -5,7 +5,7 @@ BudgetMaster de.deadlocker8 - 2.15.0 + 2.15.1 4.0.0 @@ -40,7 +40,7 @@ 2.12.1 0.8.10 5.7.1 - 1.13.4 + 1.13.5 4.0.0 5.9.3 diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/authentication/WebSecurityConfig.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/authentication/WebSecurityConfig.java index 8e189f734..c2fa6bfa6 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/authentication/WebSecurityConfig.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/authentication/WebSecurityConfig.java @@ -3,6 +3,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; @@ -20,34 +21,30 @@ public BCryptPasswordEncoder passwordEncoder() return new BCryptPasswordEncoder(); } - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf() - .and() - - .authorizeHttpRequests() - .requestMatchers("/css/**", "/js/**", "/images/**", "/webjars/**", "/favicon.ico", "/touch_icon.png").permitAll() - .requestMatchers("/login").permitAll() - .requestMatchers("/**").authenticated() - .and() - .formLogin() - .loginPage("/login") - .successHandler((req, res, auth) -> { - Object preLoginURL = req.getSession().getAttribute("preLoginURL"); - if(preLoginURL == null || preLoginUrlBlacklist.isBlacklisted(preLoginURL.toString())) - { - preLoginURL = "/"; - } - redirectStrategy.sendRedirect(req, res, preLoginURL.toString()); - }) - .permitAll() - .and() - - .logout() - .permitAll(); + .csrf(csrf -> csrf.configure(http)) + .authorizeHttpRequests(authorization -> authorization + .requestMatchers("/css/**", "/js/**", "/images/**", "/webjars/**", "/favicon.ico", "/touch_icon.png").permitAll() + .requestMatchers("/login").permitAll() + .requestMatchers("/**").authenticated()) + .formLogin(formLogin -> formLogin + .loginPage("/login") + .permitAll() + .successHandler((req, res, auth) -> { + Object preLoginURL = req.getSession().getAttribute("preLoginURL"); + if(preLoginURL == null || preLoginUrlBlacklist.isBlacklisted(preLoginURL.toString())) + { + preLoginURL = "/"; + } + redirectStrategy.sendRedirect(req, res, preLoginURL.toString()); + }) + .permitAll() + ) + .logout(LogoutConfigurer::permitAll + ); return http.build(); } diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java index 2fe7f8981..59c3a3fc1 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java @@ -47,6 +47,7 @@ public String index(Model model) public String whatsNewModal(Model model) { final List newsEntries = new ArrayList<>(); + newsEntries.add(NewsEntry.createWithLocalizationKey("csvImportBugfixMultipleTimes")); newsEntries.add(NewsEntry.createWithLocalizationKey("dockerImageSize")); newsEntries.add(NewsEntry.createWithLocalizationKey("csvImportAvoidPageReload")); newsEntries.add(NewsEntry.createWithLocalizationKey("csvImportBugfixFloatingPointPrecision")); diff --git a/BudgetMasterServer/src/main/resources/config/templates/settings.properties b/BudgetMasterServer/src/main/resources/config/templates/settings.properties index 90e5c081d..2f69a16dd 100644 --- a/BudgetMasterServer/src/main/resources/config/templates/settings.properties +++ b/BudgetMasterServer/src/main/resources/config/templates/settings.properties @@ -18,7 +18,7 @@ budgetmaster.database.password=budgetmaster server.ssl.keyStoreType=PKCS12 # set to true if you want to use SSL (highly recommended) -security.require-ssl=false +server.ssl.enabled=false # insert path to the keystore (relative to the location of BudgetMaster jar/exe or as absolute path) server.ssl.key-store= server.ssl.key-store-password= diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties index c0721a1a8..909ca7e83 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_de.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties @@ -1,7 +1,7 @@ locale=de # DEFAULT -credits=Verwendete Schriftarten: Roboto
Verwendete Bibliotheken:
spring-boot-starter-parent 3.1.0
spring-boot-devtools 3.1.0
spring-boot-starter-web 3.1.0
spring-boot-starter-test 3.1.0
spring-boot-starter-security 3.1.0
spring-boot-starter-freemarker 3.1.0
spring-boot-starter-validation 3.1.0
spring-boot-starter-data-jpa 3.1.0
hibernate-jpamodelgen 6.1.7.Final
jakarta.xml.bind-api 4.0.0
maven-surefire-plugin 2.22.2
launch4j-maven-plugin 1.7.25
jquery 3.6.4
materialize 1.0.0
fontawesome 6.4.0
Google Material Icons
Vanilla-picker 2.12.1
SortableJS 1.15.0
jlibs 3.2.0
itextpdf 5.5.13.3
mousetrap 1.6.5
plotly 2.24.2
momentjs 2.29.4
codemirror 5.62.2
webjars-locator 0.46
libUtils 3.2.7
libStorage 3.2.3
natorder 1.1.3
jgit 6.6.0.202305301015-r
opencsv 5.7.1
datatables 1.13.4
+credits=Verwendete Schriftarten: Roboto
Verwendete Bibliotheken:
spring-boot-starter-parent 3.1.1
spring-boot-devtools 3.1.1
spring-boot-starter-web 3.1.1
spring-boot-starter-test 3.1.1
spring-boot-starter-security 3.1.1
spring-boot-starter-freemarker 3.1.1
spring-boot-starter-validation 3.1.1
spring-boot-starter-data-jpa 3.1.1
hibernate-jpamodelgen 6.1.7.Final
jakarta.xml.bind-api 4.0.0
maven-surefire-plugin 2.22.2
launch4j-maven-plugin 1.7.25
jquery 3.6.4
materialize 1.0.0
fontawesome 6.4.0
Google Material Icons
Vanilla-picker 2.12.1
SortableJS 1.15.0
jlibs 3.2.0
itextpdf 5.5.13.3
mousetrap 1.6.5
plotly 2.24.2
momentjs 2.29.4
codemirror 5.62.2
webjars-locator 0.46
libUtils 3.2.7
libStorage 3.2.3
natorder 1.1.3
jgit 6.6.0.202305301015-r
opencsv 5.7.1
datatables 1.13.5
folder=Deadlocker/BudgetMaster roadmap.url=https://roadmaps.thecodelabs.de/roadmap/1 github.url=https://github.com/deadlocker8/BudgetMaster diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties index a27725004..3cdeaf46b 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_en.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties @@ -1,7 +1,7 @@ locale=en # DEFAULT -credits=Fonts used: Roboto
Libraries used:
spring-boot-starter-parent 3.1.0
spring-boot-devtools 3.1.0
spring-boot-starter-web 3.1.0
spring-boot-starter-test 3.1.0
spring-boot-starter-security 3.1.0
pring-boot-starter-freemarker 3.1.0
spring-boot-starter-validation 3.1.0
spring-boot-starter-data-jpa 3.1.0
hibernate-jpamodelgen 6.1.7.Final
jakarta.xml.bind-api 4.0.0
maven-surefire-plugin 2.22.2
launch4j-maven-plugin 1.7.25
jquery 3.6.4
materialize 1.0.0
fontawesome 6.4.0
Google Material Icons
Vanilla-picker 2.12.1
SortableJS 1.15.0
jlibs 3.2.0
itextpdf 5.5.13.3
mousetrap 1.6.5
plotly 2.24.2
momentjs 2.29.4
codemirror 5.62.2
webjars-locator 0.46
libUtils 3.2.7
libStorage 3.2.3
natorder 1.1.3
jgit 6.6.0.202305301015-r
opencsv 5.7.1
datatables 1.13.4
+credits=Fonts used: Roboto
Libraries used:
spring-boot-starter-parent 3.1.1
spring-boot-devtools 3.1.1
spring-boot-starter-web 3.1.1
spring-boot-starter-test 3.1.1
spring-boot-starter-security 3.1.1
pring-boot-starter-freemarker 3.1.1
spring-boot-starter-validation 3.1.1
spring-boot-starter-data-jpa 3.1.1
hibernate-jpamodelgen 6.1.7.Final
jakarta.xml.bind-api 4.0.0
maven-surefire-plugin 2.22.2
launch4j-maven-plugin 1.7.25
jquery 3.6.4
materialize 1.0.0
fontawesome 6.4.0
Google Material Icons
Vanilla-picker 2.12.1
SortableJS 1.15.0
jlibs 3.2.0
itextpdf 5.5.13.3
mousetrap 1.6.5
plotly 2.24.2
momentjs 2.29.4
codemirror 5.62.2
webjars-locator 0.46
libUtils 3.2.7
libStorage 3.2.3
natorder 1.1.3
jgit 6.6.0.202305301015-r
opencsv 5.7.1
datatables 1.13.5
folder=Deadlocker/BudgetMaster roadmap.url=https://roadmaps.thecodelabs.de/roadmap/2 github.url=https://github.com/deadlocker8/BudgetMaster diff --git a/BudgetMasterServer/src/main/resources/languages/news_de.properties b/BudgetMasterServer/src/main/resources/languages/news_de.properties index 8fedcd866..c13e4ffed 100644 --- a/BudgetMasterServer/src/main/resources/languages/news_de.properties +++ b/BudgetMasterServer/src/main/resources/languages/news_de.properties @@ -2,6 +2,9 @@ news.further.information=Weitere Informationen news.all.releases=Alle veröffentlichten und geplanten Versionen: news.detailed=Ausführliches Changelog (nur auf Englisch): +news.csvImportBugfixMultipleTimes.headline=Bugfix: Buchungen werden nicht mehr doppelt gespeichert +news.csvImportBugfixMultipleTimes.description=Behebt einen Fehler, der dazu führte, dass Buchungen beim CSV-Import mehrfach gespeichert wurden. + news.dockerImageSize.headline=Größe des Docker-Images reduziert news.dockerImageSize.description=Deutliche Reduzierung der Größe des Docker-Images. diff --git a/BudgetMasterServer/src/main/resources/languages/news_en.properties b/BudgetMasterServer/src/main/resources/languages/news_en.properties index b19f2acea..0a16f09e8 100644 --- a/BudgetMasterServer/src/main/resources/languages/news_en.properties +++ b/BudgetMasterServer/src/main/resources/languages/news_en.properties @@ -2,6 +2,9 @@ news.further.information=Further information news.all.releases=All published and planned releases: news.detailed=Detailed changelog (english only): +news.csvImportBugfixMultipleTimes.headline=Bugfix: Transactions are no longer saved multiple times +news.csvImportBugfixMultipleTimes.description=Fixed an error that caused transactions to be saved multiple times during CSV import. + news.dockerImageSize.headline=Improve docker image size news.dockerImageSize.description=Drastically reduce the size of the docker image. diff --git a/BudgetMasterServer/src/main/resources/static/js/transactionImport.js b/BudgetMasterServer/src/main/resources/static/js/transactionImport.js index cc29ec50a..80fcbb31a 100644 --- a/BudgetMasterServer/src/main/resources/static/js/transactionImport.js +++ b/BudgetMasterServer/src/main/resources/static/js/transactionImport.js @@ -9,12 +9,12 @@ $(document).ready(function() $('#table-transaction-rows').DataTable({ paging: false, - order: [[1, 'desc']], + order: [[2, 'desc']], info: false, scrollX: true, scrollY: false, columnDefs: [ - { orderable: false, targets: 5} + { orderable: false, targets: 6} ], language: { search: '' , searchPlaceholder: localizedSearch}, }); @@ -56,34 +56,42 @@ function initCsvTransactionForms() for(let i = 0; i < forms.length; i++) { let form = forms[i]; - $(form).submit(function(event) + console.log(i) + console.log(form) + // form.removeEventListener('submit', submitTransactionInPlaceForm); + form.addEventListener('submit', submitTransactionInPlaceForm); + } +} + +function submitTransactionInPlaceForm(event) +{ + const form = event.target; + console.log('form ' + form) + const csvTransactionId = form.dataset.index; + console.log('go') + + $.ajax({ + type: 'POST', + url: $(this).attr('action'), + data: new FormData(form), + processData: false, + contentType: false, + success: function(response) + { + $('#transaction-import-row-' + csvTransactionId).replaceWith(response); + initCsvTransactions(); + }, + error: function(response) { - const csvTransactionId = form.dataset.index; - - $.ajax({ - type: 'POST', - url: $(this).attr('action'), - data: new FormData(form), - processData: false, - contentType: false, - success: function(response) - { - $('#transaction-import-row-' + csvTransactionId).replaceWith(response); - initCsvTransactions(); - }, - error: function(response) - { - M.toast({ - html: "Error saving transaction", - classes: 'red' - }); - console.error(response); - } + M.toast({ + html: "Error saving transaction", + classes: 'red' }); + console.error(response); + } + }); - event.preventDefault(); - }); - } + event.preventDefault(); } function initCsvTransactionButtons() @@ -92,23 +100,29 @@ function initCsvTransactionButtons() for(let i = 0; i < buttonsSkip.length; i++) { const button = buttonsSkip[i]; - button.addEventListener('click', function() - { - performCsvTransactionGetRequestWithoutReload(button, 'Error skipping transaction'); - }); + button.removeEventListener('click', skipRow); + button.addEventListener('click', skipRow); } const buttonsUndoSkip = document.getElementsByClassName('button-request-transaction-import-undo-skip'); for(let i = 0; i < buttonsUndoSkip.length; i++) { const button = buttonsUndoSkip[i]; - button.addEventListener('click', function() - { - performCsvTransactionGetRequestWithoutReload(button, 'Error undo skip transaction'); - }); + button.removeEventListener('click', undoSkipRow); + button.addEventListener('click', undoSkipRow); } } +function skipRow(event) +{ + performCsvTransactionGetRequestWithoutReload(event.currentTarget, 'Error skipping transaction'); +} + +function undoSkipRow(event) +{ + performCsvTransactionGetRequestWithoutReload(event.currentTarget, 'Error undo skip transaction'); +} + function performCsvTransactionGetRequestWithoutReload(button, errorMessage) { const url = button.dataset.url; diff --git a/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl b/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl index 78cce0aed..ad57d9ac3 100644 --- a/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl +++ b/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl @@ -13,14 +13,14 @@ -<#macro customSelectEnd inputName selectedItem> - value="${selectedItem.getID()?c}"/> +<#macro customSelectEnd inputName selectedItem form=""> + value="${selectedItem.getID()?c}" <#if form?has_content>form="${form}"/> -<#macro customCategorySelect categories selectedCategory inputClasses labelText id showName=true rowClasses="" disabled=false> +<#macro customCategorySelect categories selectedCategory inputClasses labelText id showName=true rowClasses="" disabled=false form=""> <@customSelectStart "category-select-wrapper" categories inputClasses labelText id "label" disabled rowClasses>
@@ -52,7 +52,7 @@
- <@customSelectEnd "category" selectedCategory/> + <@customSelectEnd "category" selectedCategory form/> <#macro customAccountSelect selector inputName accounts selectedAccount inputClasses labelText id disabled=false> diff --git a/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl b/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl index 4b8ee3807..808412c4e 100644 --- a/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl +++ b/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl @@ -130,8 +130,8 @@ -<#macro buttonSubmit name icon localizationKey id="" color="background-blue" classes="" disabled=false formaction="" value="" onclick=""> - diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl index 34cda1496..21f8a5099 100644 --- a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl +++ b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl @@ -8,7 +8,7 @@ <@header.style "transactionImport"/> <@header.style "collapsible"/> <#import "/spring.ftl" as s> - + <@header.body> <#import "../helpers/navbar.ftl" as navbar> @@ -27,6 +27,10 @@
+ + <@header.content>
<#if csvRows??> @@ -69,7 +73,7 @@ <#import "../helpers/scripts.ftl" as scripts> <@scripts.scripts/> - + diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl index cf03a21ed..b383e17c5 100644 --- a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl +++ b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl @@ -168,6 +168,7 @@ + @@ -185,25 +186,30 @@
${locale.getString("transactions.import.status")} ${locale.getString("transaction.new.label.date")} ${locale.getString("transaction.new.label.category")}
+ + <@newTransactionMacros.insertNameSuggestions/> <#macro renderCsvTransaction csvTransaction index> -
- + + + +
+ <@statusBanner csvTransaction.getStatus()/> ${csvTransaction.getDate()} - <@customSelectMacros.customCategorySelect categories csvTransaction.getCategory() "left-align no-margin-top no-margin-bottom" "" "csvTransaction-category-${index}" false "no-margin-bottom" csvTransaction.getStatus().name() == 'SKIPPED'/> + <@customSelectMacros.customCategorySelect categories csvTransaction.getCategory() "left-align no-margin-top no-margin-bottom" "" "csvTransaction-category-${index}" false "no-margin-bottom" csvTransaction.getStatus().name() == 'SKIPPED' 'newTransactionInPlace_${index}'/>
- disabled> + disabled>
- disabled> + disabled>
${currencyService.getCurrencyString(csvTransaction.getAmount())} @@ -211,7 +217,7 @@ <#if csvTransaction.getStatus().name() == 'SKIPPED'> <@header.buttonFlat url='/transactionImport/' + index + '/undoSkip' isDataUrl=true icon='do_disturb_off' localizationKey='' classes="no-padding text-default button-request-transaction-import-undo-skip" datasetIndex=index/> <#else> - <@header.buttonSubmit name='action' icon='save' localizationKey='' classes='text-white'/>  + <@header.buttonSubmit name='action' icon='save' localizationKey='' classes='text-white' form='newTransactionInPlace_${index}'/> 
edit @@ -236,8 +242,6 @@ - - <@newTransactionMacros.insertNameSuggestions/> <#macro showColumnSettingsErrors> diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java index 70ea4e380..28fec6532 100644 --- a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java +++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java @@ -642,23 +642,23 @@ private static void assertRow(WebElement row, String statusColor, String date, S { final List columns = row.findElements(By.tagName("td")); - assertThat(columns.get(0).findElements(By.cssSelector(".banner.background-" + statusColor))) + assertThat(columns.get(1).findElements(By.cssSelector(".banner.background-" + statusColor))) .hasSize(1); - assertThat(columns.get(1).getText()) + assertThat(columns.get(2).getText()) .isEqualTo(date); - final WebElement categoryCircle = columns.get(2).findElement(By.className("category-circle")); + final WebElement categoryCircle = columns.get(3).findElement(By.className("category-circle")); categoryName = categoryName.substring(0, 1).toUpperCase(); assertThat(categoryCircle.findElement(By.tagName("span"))).hasFieldOrPropertyWithValue("text", categoryName); - assertThat(columns.get(3).findElement(By.name("name")).getAttribute("value")) + assertThat(columns.get(4).findElement(By.name("name")).getAttribute("value")) .isEqualTo(name); - assertThat(columns.get(4).findElement(By.name("description")).getAttribute("value")) + assertThat(columns.get(5).findElement(By.name("description")).getAttribute("value")) .isEqualTo(description); - assertThat(columns.get(5).getText()) + assertThat(columns.get(6).getText()) .isEqualTo(amount); } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a22fa6603..e8ba61964 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG APP_DIR=/BudgetMaster RUN mkdir -p $APP_DIR RUN mkdir -p /root/.Deadlocker/BudgetMaster -COPY BudgetMasterServer/build/2.15.0/BudgetMasterServer-v2.15.0.jar /BudgetMaster/BudgetMaster.jar +COPY BudgetMasterServer/build/2.15.1/BudgetMasterServer-v2.15.1.jar /BudgetMaster/BudgetMaster.jar COPY BudgetMasterServer/src/main/resources/config/templates/settings-docker.properties /root/.Deadlocker/BudgetMaster/settings.properties RUN echo "server.port=9000" > ~/.Deadlocker/BudgetMaster/settings.properties diff --git a/README.md b/README.md index 5a372d77e..a81661aba 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Manage your monthly budget easily with BudgetMaster - __start:__ 17.12.16 -- __current release:__ v2.15.0 (43) from 21.06.23 +- __current release:__ v2.15.1 (44) from 18.07.23 ## Key Features - Keep your data private - Host your own BudgetMaster server or use it in standalone mode. All data remains on your machines. diff --git a/docker-compose.yaml b/docker-compose.yaml index b539481d2..387c92e61 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,9 +6,9 @@ services: volumes: - "./data/budgetmaster:/root/.Deadlocker/BudgetMaster" expose: - - "8080" + - "9000" ports: - - "8080:8080" + - "9000:9000" networks: - netPostgres environment: diff --git a/pom.xml b/pom.xml index 96f6b691b..493b63beb 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.deadlocker8 BudgetMaster pom - 2.15.0 + 2.15.1 BudgetMaster @@ -40,7 +40,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.0 + 3.1.1 @@ -51,7 +51,7 @@ ${maven.build.timestamp} dd.MM.yy - 43 + 44 Robert Goldmann 1.18.3