Giter VIP home page Giter VIP logo

d-r-q / trainer-advisor Goto Github PK

View Code? Open in Web Editor NEW
11.0 2.0 6.0 43.96 MB

Информационная система йогатерапевта и тренера смарт-фитнеса, демонстрационный проект Эргономичного подхода

Home Page: https://trainer-advisor.pro

Kotlin 49.41% Dockerfile 0.01% CSS 21.86% HTML 12.54% JavaScript 15.75% Shell 0.17% Jinja 0.26%
data-oriented-programming functional-architecture modular-monolith spring-data-jdbc tdd spring spring-boot immutable-domain-model

trainer-advisor's Introduction

Trainer Advisor

Информационная система йогатерапевта.

Данный проект является иллюстрацией кодовой базы, выполненной в соответствии с Эргономичным подходом.

Посты с описанием проекта можно найти у меня в блоге и Telegram-канале.

Требования к инфраструктуре

  • JDK: 21 (Temurin)
  • PostgreSQL 15.2

Требования к рабочему окружению

  • Docker
  • Docker Compose

Локальная разработка

TDD

Разработку рекомендуется вести преимущественно через TDD - то есть реализация продового кода ведётся в рамках " озеленения" теста.

Запуск инфраструктуры

Опционально можно руками запустить инфраструктуру для тестов c помощью IDEA Run Configuration (далее - ран конфига) " Infra - Tests - Up":

img.png

Это позволит сэкономить полсекунды на запуск тестов, за счёт пропуска инициализации testcontainers.

Вообще у IDEA есть специализированные ран конфиги для Docker Compose, но у меня идея крэшится при их использовании

Остановить инфраструктуру можно ран конфигом "Infra - Tests - Down".

Запуск одного теста

В случае простых/небольших фич и багфиксов можно ограничиться запуском одного класса или теста через гуттер

img.png

Запуск быстрых тестов

Если изменения затрагивают несколько частей системы, их разработку можно драйвить с помощью ран конфига "Tests - Fast":

img.png

Эта конфигурация запускает все тесты, не отмеченные тегом "slow" и её есть смысл запускать, если в рамках решения задачи, был затронут большой процент кода проекта.

Запуск всех тестов

Если разработка совсем вышла из-под контроля и починка одного теста влечёт падение другого, то лучше перейти на ран конфиг "Tests - All":

img.png

Ручное тестирование

Отладка и тестирования вёрстки и динамических частей страниц выполняется в ручном режиме, поэтому для этих целей необходимо запустить проект с помощью ран конфига "QYogaApp":

img.png

В проекте настроена интеграция Spring Boot Docker Compose, поэтому для запуска проекта достаточно запустить только само приложение в IDEA.

При изменении статических файлов (и, зачастую, кода) для их обновления не обязательно перезапускать приложение и достаточно только пересобрать проект (Ctrl+F9).

При необходимости, остановить инфраструктуру можно ран конфигом "Infra - Dev - Down":

img.png

Отладка приложения в контейнере

Для отладки работы приложения в докере можно воспользоваться ран конфигом "System - Local - Up":

img.png

Этот конфиг выполняет сборку приложения в jar-файл (из текущего состояния рабочей директории), после чего собирает докер-контейнер на базе этого файла и затем в терминале запускает композ всей системы.

Композ запускается в интерактивном режиме, поэтому для остановки надо в консоли ввести Ctrl+c.

Проверка перед пушем

Перед пушем рекомендуется запускать Gradle-таск check. Этот таск выполняет все тесты и проверяет процент покрытия кода тестами. Для запуска таска можно воспользоваться ран конфигом "Tests - Check":

img.png

Локальная разработка фронта в контейнере

Запуск контейнера

Для запуска проекта необходимо в корне проекта (репозитория) выполнить команду:

docker compose -f deploy/qyoga/docker-compose-infra-base.yml -f deploy/qyoga/docker-compose-front-dev.yml up --build app 

При первом запуске будут длительные задержки - это качаяются зависимости. Повторые запуски будут проходить быстро.

В случае успешного запуска, в логах докера должна появиться строка

pro.qyoga.app.QYogaAppKt - Started QYogaAppKt in 1.938 seconds (process running for 2.224)

После этого приложение будет доступно по URL http://localhost:8080, а все изменения в HTML/CSS/JavaScript в директории src/main/resources будут видимы сразу после сохранения файла.

Пересборка контейнера

Для того чтобы обновить бэк в контейнере (напирмер, после переключения ветки), необходимо удалить старый компоуз-проект:

docker compose -f deploy/qyoga/docker-compose-infra-base.yml -f deploy/qyoga/docker-compose-front-dev.yml down

После чего запустить контейнер заново

Работа с продом

Перед работой с продом на новой машине необходимо выполнить шаги из раздела "Сетап хоста"

Деплой

Для деплоя актуальной версии приложения необходимо выполнить скрипт:

deploy/host/deploy.sh

Этот скрипт подключится к серверу, загрузит последний образ приложения и задеплоит его.

Просмотр логов

deploy/host/logs.sh

Обновление сертификата

deploy/host/update-cert.sh

Сетап хоста

deploy/host/setup-host.sh

Сетап нового сервера

deploy/host/setup-server.sh

trainer-advisor's People

Contributors

d-r-q avatar daemonfoen avatar mmalakhova avatar nntrifonova avatar zertalian1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

trainer-advisor's Issues

Поправить включение заголовка

Из-за некорректного использования thymeleaf, заголовок включался внутрь тэга body, а не head, из-за чего в проде ломался как минимум скрипт показа меню профиля

image

Быстрое добавление особенности при заполнении карточки нового клиента

Предусловие: клиент заполнил форму регистрации

На приёме, терапевт перед опросом открывает страницу карточки клиента.
Опрашивает клиента и вносит данные в карточку.
В мультисписке с автодополнением особенности в текстовом поле поиска особенности начинает вводить название, видит, что нужной особенности в системе нет и вводит полное название особенности.

После этого терапевт нажимает Enter и открывается диалоговое окно с формой создания особенности.
В этом окне предзаполнено поле названия особенности и есть два мультисписка с автодополнением для выбора противопоказанных и ограниченных манипуляций.

Заполнив мультисписки терапевт нажимает кнопку сохранить, особенность вносится в БД, диалоговое окно закрывается, особенность добавляется в мультисписок в карточке клиента.

Примечания:

  • При поиске противопоказанных и ограниченных манипуляций их может не оказаться и тут в идеале бы открыть второе окно для быстрого создания манипуляции 😨

Примечания в бок:

  • Так же должна быть возможность отправки ссылки приглашения без назначения приёма
  • Уведомлять о регистрации клиента можно через новый модуль системы уведомлений на сайте
  • При редактировании списка упражнений в программе, должна быть возможность быстрого добавления манипуляции в упражнение и добавления одной из манипуляций упражнения в список ограничений или протвопоказаний особенности.

Сделать новогоднюю уборку

Подчистить хвосты:

  • Добавить бэкап БД в скрипт деплоя в прод
  • Мигрировать БД на проде
  • Реализовать массовую загрузку изображений
  • Доделать загрузку изображений в ExercisesService.addExercises
  • Динамический список типов упражнений на странице создания упражнения

Фича: объём движений

В карточке клиента должна быть вкладка объёма движений.

С полями:

  • Угол сгибания* ШОП, ГОП, ПОП, ТБС (правый/левый), колена, голеностопа, большой палец на ноге, ПС, локоть, лучезапястный сустав
  • Угол разгибания того же
  • Сгибание во фронтальной плоскости ГОП, ПОП, ШОП (право/лево)
  • Ротация ШОП, ГОП, ПОП, ТБС, колена, голеностоп, ПС (право/лево)
  • Элевация, депрессия, протракция, ретракция, ротация лопатки (право/лево)
  • Приведение/отведение ТБС, ПС

*отклонение от нейтрали

Все поля необязательные.

На форме должны быть подсказки с нормами объёма движений

Починить пагинацию пустого списка упражнений

Сейчас попытка открыть страницу с пустым списком упражнений крешится

2023-05-25 03:14:09.214 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 3 ms
2023-05-25 03:14:57.537 [http-nio-8080-exec-5] ERROR org.thymeleaf.TemplateEngine - [THYMELEAF][http-nio-8080-exec-5] Exception processing template "therapist/exercises/exercise-search": Exception evaluating SpringEL expression: "(exercises.getPageable().pageNumber == 0) ? 'disabled' : ''" (template: "therapist/exercises/exercise-search" - line 106, col 45)
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "(exercises.getPageable().pageNumber == 0) ? 'disabled' : ''" (template: "therapist/exercises/exercise-search" - line 106, col 45)
        at org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292)
        at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166)
        at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66)
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109)
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138)
        at org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor.doProcess(AbstractStandardExpressionAttributeTagProcessor.java:144)
        at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74)
        at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95)
        at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633)
        at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1314)
        at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205)
        at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:136)
        at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:661)
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1103)
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1077)
        at org.thymeleaf.spring6.view.ThymeleafView.renderFragment(ThymeleafView.java:372)
        at org.thymeleaf.spring6.view.ThymeleafView.render(ThymeleafView.java:192)
        at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1415)
        at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1159)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1098)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
        at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1021E: A problem occurred whilst attempting to access the property 'pageNumber': 'Unable to access property 'pageNumber' through getter method'
        at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:214)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:105)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:410)
        at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:93)
        at org.springframework.expression.spel.ast.OpEQ.getValueInternal(OpEQ.java:42)
        at org.springframework.expression.spel.ast.OpEQ.getValueInternal(OpEQ.java:32)
        at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:212)
        at org.springframework.expression.spel.ast.Ternary.getValueInternal(Ternary.java:54)
        at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114)
        at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:338)
        at org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:265)
        ... 96 common frames omitted
Caused by: org.springframework.expression.AccessException: Unable to access property 'pageNumber' through getter method
        at org.springframework.expression.spel.support.ReflectivePropertyAccessor$OptimalPropertyAccessor.read(ReflectivePropertyAccessor.java:686)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:209)
        ... 106 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.expression.spel.support.ReflectivePropertyAccessor$OptimalPropertyAccessor.read(ReflectivePropertyAccessor.java:682)
        ... 107 common frames omitted
Caused by: java.lang.UnsupportedOperationException: null
        at org.springframework.data.domain.Unpaged.getPageNumber(Unpaged.java:59)
        ... 112 common frames omitted
2023-05-25 03:14:57.568 [http-nio-8080-exec-5] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "(exercises.getPageable().pageNumber == 0) ? 'disabled' : ''" (template: "therapist/exercises/exercise-search" - line 106, col 45)] with root cause
java.lang.UnsupportedOperationException: null
        at org.springframework.data.domain.Unpaged.getPageNumber(Unpaged.java:59)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.expression.spel.support.ReflectivePropertyAccessor$OptimalPropertyAccessor.read(ReflectivePropertyAccessor.java:682)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:209)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:105)
        at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:410)
        at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:93)
        at org.springframework.expression.spel.ast.OpEQ.getValueInternal(OpEQ.java:42)
        at org.springframework.expression.spel.ast.OpEQ.getValueInternal(OpEQ.java:32)
        at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:212)
        at org.springframework.expression.spel.ast.Ternary.getValueInternal(Ternary.java:54)
        at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114)
        at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:338)
        at org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:265)
        at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166)
        at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66)
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109)
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138)
        at org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor.doProcess(AbstractStandardExpressionAttributeTagProcessor.java:144)
        at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74)
        at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95)
        at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633)
        at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1314)
        at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205)
        at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:136)
        at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:661)
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1103)
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1077)
        at org.thymeleaf.spring6.view.ThymeleafView.renderFragment(ThymeleafView.java:372)
        at org.thymeleaf.spring6.view.ThymeleafView.render(ThymeleafView.java:192)
        at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1415)
        at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1159)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1098)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
        at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.base/java.lang.Thread.run(Thread.java:831)

Исправить редирект после логина на js/scripts.js

Из-за некорректной конфигруации авторизации, после успешного логина выполняется редирект на js/scripts.js.

По дефолту после логина спринг выполняет редирект на последний назпрошенный защищённый ресрус, и т.к. доступ к скриптам не был открыт - спринг редиректил туда.

Маска ввода даты

На странице создания клиента терапевтом надо добавить маску ввода даты. Чтобы из хх.хх.хххх, пользователь руками вводил только х-ы

Первичная консультация

Терапевту вне системы поступает запрос на первичную консультацию. Терапевт в календаре выбирает время приёма. На форме создания приёма, терапевт нажимает кнопку "новый клиент" и получает ссылку на "входную анкету" для нового клиента, содержащую:

  • емейл
  • телефон
  • фио
  • Жалобы
    • описание
    • когда появилось
    • что предшествовало появлению жалобы
    • что облегчает боль
    • что усугубляет боль
    • пробовали ли медикаментозное лечени (текст - какие медикаменты, помогло/нет)
    • Что ещё пробовали делать для облегчения симптомов
  • Травмы и операции

После генерации ссылки, в системе создаётся приём на выбранное время.
В календаре приёмы для которых не задан клиент или тип занятия выделяются цветом (розовый).

Вне системы терапевт передаёт ссылку клиенту. Клиент заполняет входную анкету (карточку).
После отправки анекты, в системе создаётся карточка нового клиента, приём становится заполненным и терапевта просматривая календарь в рамках ежедневной рутины обращает внимание, на то, что приём стал заполненным.

После этого терапевт в календаре заходит в приём и оттуда проваливается в карточку клиента.
Смотрит на жалобы, в результате у терапевта могут возникнуть дополнительные вопросы, которые он задаёт в воцапе, кликнув по кнопке в карточке клиента. Также терапевт может сгенерировать 0 или более ссылок на опросники и отправить их также по воцапу.

В случае наличия вопросов, после получения ответов на вопросы в вацапе, терапевт их руками переносит в карточку клиента.

В случае наличия опросников, клиент по очереди переходит по каждой ссылке, заполняет опросник и после этого заполненный опросник автоматически прикрепляется к карточке клиента.

Далее, терапевт смотрит результаты опросников, жалобы и формирует клиническую гипотезу, которую вносит в журнал в карточке клиента. Исходя из клинической гипотезы, терапевт планирует какие тесты будет делать для подтверждения или опровержения клинической гипотезы.

В день приёма клиент приходит на занятие.
Терапевт устно уточняет все данные и вносит правки в карточку клиента.
Дополнительно терапевт замеряет объём движений в основных суставах клиента и вносит результаты в карточку клиента (на отдельную закладку).
Также после визуального осмотра и беседы, терапевт вносит в карточку особенности (патологии) клиента.

Затем терапевт выполняет запланированные тесты и получает либо подтверждение, либо опровержение гипотезы. Цикл гипотеза-тесты повторяется до получения подтверждённой клинической гипотезы - диагноза.
Диагноз вносится в карточку клиента.

В карточке клиента у каждого элемента списка диагнозов есть кнопка "сформировать программу".
Нажав на кнопку около одного из диагнозов, терапевт попадает в редактор программ.

В редакторе предзаполнен список упражнений на основании данных из карточки клиента - противопоказаних всех диагнозов, показаний выбранного диагноза и особенностей клиента.
Терапевт редактирует программу:

  • упорядочивает упражнения
  • удаляет упражнения
  • добавляет упражнения
  • изменяет протокол выполнения упражнения (список выбора + текстовое поле в карточке упражнения)
  • изменяет описание упражнения в программе

Завершив редактирование, терапевт сохраняет программу, после чего она привязывается к карточке клиента.
У каждой программы в карточке клиента есть кнопка "отправить клиенту", нажатие которой приводит к:

  • отправке pdf-файла с программой на емейл клиента
  • добавлению в журнал клиента записи о генерации программы со ссылкой на просмотр программы

Терапевт создаёт задачу "отправить сообщение клиенту" с планируемым текстом сообщения и датой отправки

Технические заметки:

  • ссылка должна быть публично доступной, но одноразовой
  • журнал - это просто список текстов с датами, привязанный к карточке клиента.

Реализовать страницу создания клиента

Ветка: https://github.com/d-r-q/QYoga/tree/qg-81/create-client-page

Этапы

  1. Вёрстка
  2. Тест вёрстки
  3. Реализация эндпоинта сохранения

Вёрстка

definition of done:

  • В систему добавлена страница создания клиента
    • Выдаётся по GET /therapist/clients/create
    • Страница сразу генерируется thymeleaf и использует существующие фрагменты
    • Страница содержит все элементы из макета РК-6, кроме "Особенности"
    • Страница "аккуратная" (см. создания упражнения)

Тест вёрстки

dod:

  • Добавлен тест вёрстки страницы
    • Добавлен метод TherapistClientsApi.getCreateClientPage()
    • Добавлен объект CreateClientPage
    • Объект CreateClientPage проверяет все элементы страницы

Реализация эндпоинта сохранения

dod:

  • Добавлен метод TherapistClientsApi.createClient(createClientRequest)
  • Добавлен тест добавления клиента
  • Тест верифицирует появление клиента через вызов метода эндпоинта контроллера (скрытого в ClientsBackgrounds)
  • Реализована логика сохранения клиента в БД

Реализовать страницу заявки на регистрацию

dod

  • На странице логина добавлена ссылка на страницу заявки на регистрацию
  • На странице заявки пользователь вводит свой емейл и нажимает кнопку "Отправить"
  • При обработке запроса, создаётся новый пользователь с указанным емейлом, ролью "терапевт" и случайным паролем, после чего емейл и пароль отправляются на почту администратора
  • После обработки запроса, пользователь может войти в систему и получить доступ к приложению терапевта
  • В случае, если пользователь с переданным емейлом уже существует, на форме заявки должно отобразиться сообщение об ошибке с предложением обратится к администратору для восстановления пароля

Маска ввода телефона

На странице создания клиента терапевтом надо добавить маску ввода телефона. Чтобы из +7-ххх-ххх-хх-хх, пользователь руками вводил только х-ы

Провиженинг сервера для qyoga

Надо завести набор скриптов/инструментов, для подготовки нового сервера к деплою приложения, собственно деплоя приложения на сервер и подготовки хост-системы к подготовке и деплою приложения на сервер

Добавить общий обработчик ошибок для htmx

Надо, чтобы в случае неожиданных статус кодов ответов htmx-запросов отображалась какая-нибудь нотификация аля "Что-то пошло не так, попробуйте позднее".

А в случае 401 - редирект на форму логина

Починить крэш получения сессии при запуске в докере

На проде любой запрос валится с

2023-05-21 08:54:26.454 [http-nio-8080-exec-5] ERROR o.a.c.c.C.[Tomcat].[localhost] - Exception Processing ErrorPage[errorCode=0, location=/error]
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES
FROM SPRING_SESSION S
LEFT JOIN SPRING_SESSION_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID
WHERE S.SESSION_ID = ?
]; ERROR: invalid byte sequence for encoding "UTF8": 0x00
  Where: unnamed portal parameter $1
        at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:105)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79)
        at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1538)
        at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:667)
        at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:713)
        at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:744)
        at org.springframework.session.jdbc.JdbcIndexedSessionRepository.lambda$findById$1(JdbcIndexedSessionRepository.java:479)
        at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
        at org.springframework.session.jdbc.JdbcIndexedSessionRepository.findById(JdbcIndexedSessionRepository.java:478)
        at org.springframework.session.jdbc.JdbcIndexedSessionRepository.findById(JdbcIndexedSessionRepository.java:139)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getRequestedSession(SessionRepositoryFilter.java:351)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:285)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:194)
        at jakarta.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:221)
        at jakarta.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:221)
        at org.springframework.web.util.WebUtils.getSessionId(WebUtils.java:360)
        at org.springframework.web.servlet.FrameworkServlet.publishRequestHandledEvent(FrameworkServlet.java:1149)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1028)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
        at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
        at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:143)
        at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
        at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:642)
        at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:410)
        at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:340)
        at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:277)
        at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:358)
        at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:222)
        at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:304)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:149)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: org.postgresql.util.PSQLException: ERROR: invalid byte sequence for encoding "UTF8": 0x00
  Where: unnamed portal parameter $1
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2676)
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2366)
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:356)
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:496)
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:413)
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:190)
        at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:134)
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
        at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:722)
        at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651)
        ... 88 common frames omitted

Перевести хранение изображений на minio

Ветка: https://github.com/d-r-q/QYoga/tree/qg-80/migrate-to-minio

Этапы

  1. Рефакторинг ImagesService -> FilesService
  2. Сетап контейнеров
  3. Реализация MinioFilesService
  4. Реализация массовой загрузки

Рефакторинг ImagesService -> FilesService

definition of done:

  • Таблица images переименована в files
  • Переименованы в File* классы Image, ImageService, ImageServiceImpl.
    • Переменные и парметры в пакете pro.qyoga.programs остались image

Сетап контейнеров

dod:

  • В докер компоуз файлы добавлен контейнер минио
    • В tests-infra добавлен tmpfs том (tmpfs) (в ручную проверено временем запуска тестов, то что это ускоряет их работу)
    • В продовом файле для контейнера определён том из хоста (volumes)
    • В файлах tests-infra и dev-infra контейнеры выставляют разные порты
  • В тестовой инфраструктуре повторён трюк для постгреса, с проверкой наличия предзапущенного контейнера и откатом на запуск контейнера через testcontainers, если его нет
  • Ран конфиги продолжают работать
    • Tests - All
    • Infra - Tests - Up/Down
    • Infra - Dev - Up/Down
    • System - Local - Up/Down
    • System - Last Release - Up/Down

Реализация MinioFilesService

dod:

  • Добавлен класс MinioFilesService реализующий FilesService, который хранит мета инфу (название, тип, размер) в таблице files, а сами файлы - в минио
  • Добавлен класс ExerciseStepImagesMigrator, который считывает таблицу files и переносит файлы в minio
  • В тестах нет изменений (?)

Реализация массовой загрузке

dod:

  • В интерфейс FilesStorage добавлен метод uploadAll(files: List): List
  • Этот метод реализован в MinioFilesService
    • Вставка в постгрес осуществляется пакетно
    • При наличии возможности - загрзука в минио осуществляется пакетно
    • Метод возвращает список объектов с проставленными ИДами
  • Методы создания клиентов в ExercisesServiceImpl перевдены на использование метод массовой загрузки изображений

Разделить противопоказания на абсолютные и относительные

Абсолютные противопоказания полностью исключают манипуляцию (например перевёрнутые положения при сердечной недостаточности), относительные ПП доупскают использование манипуляции но в ограниченном количестве, так как это будет усугублять особенность (опора на ладони со сгибанием плечевого сустава при напряжённых грудных мышцах)

Поправить включение фрагментов

Сейчас почему-то внутри контейнера фрагменты, которые включение которых начинается с / приводит к поломки thymeleaf-а:

2023-05-04 06:31:44.617 [http-nio-8080-exec-3] DEBUG o.s.web.servlet.DispatcherServlet - Failed to complete request: org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/fragments/footer.html], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "clients-list" - line 172, col 47)
2023-05-04 06:31:44.619 [http-nio-8080-exec-3] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/fragments/footer.html], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "clients-list" - line 172, col 47)] with root cause
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/fragments/footer.html], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "clients-list" - line 172, col 47)
	at org.thymeleaf.engine.TemplateManager.resolveTemplate(TemplateManager.java:869)
	at org.thymeleaf.engine.TemplateManager.parseStandalone(TemplateManager.java:250)

Надо починить

Добавить обработку нарушения уникальности телефона клиента

Сейчас при попытке создать клиента с телефоном, который уже есть у другого клиента - вылетит 500-ка.

А должна быть человеческая ошибка, с сохранением данных в форме.

А ещё лучше - фоновая проверка после потери фокуса инпутом телефона

Навести порядок в урлах

Сейчас в урлах полный бордак:

  • Есть открытые страницы в корне - /login
  • Есть открытые страницы в /user - /users/login
  • Есть закрытые страницы в корне - /clients

Надо все открытые страницы оставить в корне, а закрытые перенести в соответствующие приложения (только therapist на данный момент)

Индивидуальные занятия на регулярной основе

Перед занятием терапевт просматривает журнал - что делали на прошлых занятиях, какой план на текущее занятие, какие запланированы тесты.

На занятии терапевт выполняет тесты (все или часть запланированных, незапланированные). Затем терапевт проводит занятие. После занятия терапевт вносит в систему:

  • результаты тестов
  • какие упражнения были выполнены на занятии
  • какие были сложности (текстовое поле)
  • наброски плана на следующее занятие
  • задачу "Отправить сообщение клиенту", с планируемым тектом сообщения и датой выполнения
  • оценка клиентом самочувствия до и после занятия

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.