From 436a6172e61e3fb33a0d6f9f85cd146627ecc9ce Mon Sep 17 00:00:00 2001 From: madeliri Date: Sat, 6 Jun 2026 11:44:08 +0300 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20BREAKING=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D0=B0=D0=BC=D0=B8=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20-=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=83?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=20=D1=81=20`/config/schemeas/`=20=D0=B4?= =?UTF-8?q?=D0=BE=20`/schemas/'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{configs => }/schemas/example_of_scheme.xlsx | Bin modules/global_options.R | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename example_scheme/{configs => }/schemas/example_of_scheme.xlsx (100%) diff --git a/example_scheme/configs/schemas/example_of_scheme.xlsx b/example_scheme/schemas/example_of_scheme.xlsx similarity index 100% rename from example_scheme/configs/schemas/example_of_scheme.xlsx rename to example_scheme/schemas/example_of_scheme.xlsx diff --git a/modules/global_options.R b/modules/global_options.R index c6b63e7..96231c1 100644 --- a/modules/global_options.R +++ b/modules/global_options.R @@ -55,7 +55,7 @@ check_and_init_scheme = function() { ) scheme_names <- names(config::get()$form_schemes) - scheme_file <- paste0(config::get("form_app_configure_path"), "/configs/schemas/", scheme_names, ".xlsx") + scheme_file <- paste0(config::get("form_app_configure_path"), "/schemas/", scheme_names, ".xlsx") scheme_file <- stats::setNames(scheme_file, scheme_names) if (!all(file.exists(scheme_file))) { From 39ae2337d4e61bdc205f95d2d9263bcd40459103 Mon Sep 17 00:00:00 2001 From: madeliri Date: Sat, 6 Jun 2026 14:09:32 +0300 Subject: [PATCH 2/6] 0.18.0 --- CHANGELOG.md | 4 ++ README.md | 4 +- app.R | 104 ++++++++++++++++++++++++++++++++++-------- utils/init_login_db.r | 7 +-- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88165ca..9e5f9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 0.18.0 (2026-06-06) +##### features +- возможность отрганичить доступ к базам данных для отдельных пользователей + ### 0.17.0 (2026-04-24) ##### features - модуль с задачами: для каждой записи в базе можно создать задачи, на главном экране отображается общее количество активных задач, по сроку выполнения на сегодня и просроченные задачи; diff --git a/README.md b/README.md index 3d2ccfa..ac317ba 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ git clone https://gitea.madelirihs.ru/madeliri/shiny_form.git renv::init() ``` -# Насторйки +# Настройки ## переменные окружения @@ -46,7 +46,7 @@ FORM_APP_LOCAL_DB_BACKUP_PATH="path_to_backups" Проверка осуществляется при каждом запуске приложения, бэкапы создаются раз в день (при первом запуске). -Количество сохраняемых бэкапов: +Количество послдних сохраненных бэкапов: ``` FORM_APP_LOCAL_DB_BACKUP_LIMITS=3 ``` diff --git a/app.R b/app.R index fb66e09..f0b8b3b 100644 --- a/app.R +++ b/app.R @@ -17,7 +17,8 @@ box::use( modules/db, modules/data_validation, app/forms, - app/tasks + app/tasks, + modules/data_manipulations[is_this_empty_value] ) # global settings: @@ -61,7 +62,7 @@ ui <- page_sidebar( actionButton("save_data_button", "Сохранить данные", icon("floppy-disk", lib = "font-awesome")), actionButton("clean_data_button", "Главная страница", icon("house", lib = "font-awesome")), actionButton("load_data_button", "Загрузить данные", icon("pencil", lib = "font-awesome")), - downloadButton("downloadDocx", "get .docx (test only)"), + # downloadButton("downloadDocx", "get .docx (test only)"), uiOutput("status_message"), textOutput("status_message2"), uiOutput("display_log"), @@ -72,7 +73,7 @@ ui <- page_sidebar( span( config::get("form_app_version"), fontawesome::fa("circle-info", a11y = "sem", title = "Settings"), - style = "color: #9c9c9c; position: fixed; bottom: 5px; right: 5px;"), + style = "color: #9c9c9c; position: fixed; bottom: 5px; left: 5px;"), title = "about", placement = "left", tagList(span("здесь пока ничего нет"), br(), span("вот")) @@ -126,6 +127,23 @@ server <- function(input, output, session) { NULL } + user_access <- function(string) { + + if (is_this_empty_value(string)) return(NA) + if (string == "all") return("all") + + forms_access <- stringr::str_split_1(string, ", ") + + # check if exists + exists <- forms_access %in% enabled_schemes + if (!all(exists)) { + cli::cli_warn(c("these forms is not exist:", paste("- ", forms_access[!exists]))) + } + + # возращаем схемы для которых есть доступ + forms_access[exists] + } + # важные кнопки управления output$admin_buttons_panel <- renderUI({ @@ -157,29 +175,60 @@ server <- function(input, output, session) { } }) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # REACTIVE VALUES ================================= # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Create a reactive values object to store the input data values <- reactiveValues( - data = NULL, - tasks_data = NULL, - main_key = NULL, - nested_key = NULL, - nested_form_id = NULL, - tasks_id = NULL, - current_user = NULL + data = NULL, + tasks_data = NULL, + main_key = NULL, + nested_key = NULL, + nested_form_id = NULL, + tasks_id = NULL, + current_user = NULL, + user_form_access = enabled_schemes ) - scheme <- reactiveVal(enabled_schemes[1]) # наименование выбранной схемы - mhcs <- reactiveVal(schms[[enabled_schemes[1]]]) # объект для выбранной схемы + scheme <- reactiveVal(NULL) # наименование выбранной схемы + mhcs <- reactiveVal(NULL) # объект для выбранной схемы observers_started <- reactiveVal(NULL) - main_form_is_empty <- reactiveVal(TRUE) + main_form_is_empty <- reactiveVal(NULL) validator_main <- reactiveVal(NULL) validator_nested <- reactiveVal(NULL) + # доступ к схемам + observe({ + + # определение доступа в завимости от условий (включена ли авторизация, и есть ли доступы) + res <- if (AUTH_ENABLED) { + # если администратор - полный доступ, если нет - проверка по полю + ifelse(res_auth$admin, "all", user_access(res_auth$scheme_access)) + } else { + # если нет авторизации - полный доступ + "all" + } + if(length(res) == 0) return(NA) + + # списки доступных схем + allowed_schemas <- if (is.na(res)) { + NA # нет доступа + } else if (res == "all") { + enabled_schemes # все схемы + } else { + enabled_schemes[enabled_schemes == res] # только указанные + } + + # переопределяем переменные + main_form_is_empty(ifelse(is.na(res), "empty", "main_menu")) + values$user_form_access <- allowed_schemas + scheme(values$user_form_access[1]) + mhcs(schms[[values$user_form_access[1]]]) + }) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # reactive ui ------------------------------- # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -187,15 +236,16 @@ server <- function(input, output, session) { ## reactive ui ----------------------------------- ### main screen ------ output$main_ui_navset <- renderUI({ + req(main_form_is_empty()) - if (main_form_is_empty()) { + if (main_form_is_empty() == "main_menu") { validator_main(NULL) div( h5("Выбрать базу данных для работы:"), shiny::radioButtons( "schmes_selector", label = NULL, - choices = enabled_schemes, + choices = values$user_form_access, selected = scheme() ), hr(), @@ -207,13 +257,20 @@ server <- function(input, output, session) { # загрузка панели для работы с базой данных uiOutput("admin_buttons_panel") ) - } else { + } else if (main_form_is_empty() == "form") { # list of rendered panels validator_main(data_validation$init_val(mhcs()$get_scheme("main"))) validator_main()$enable() mhcs()$get_main_form_ui + } else if (main_form_is_empty() == "empty") { + + div( + h5("Нет доступных баз данных для работы"), + p("Для данного пользователя нет доступа к формам для работы."), + p("Обратитесь к системному администратору.") + ) } }) @@ -222,7 +279,7 @@ server <- function(input, output, session) { output$base_data <- renderUI({ - if (main_form_is_empty() == TRUE) { + if (main_form_is_empty() == "main_menu") { con <- db$make_db_connection(scheme(),"base_data") on.exit(db$close_db_connection(con, "base_data"), add = TRUE) @@ -356,6 +413,7 @@ server <- function(input, output, session) { ## кнопки для каждой вложенной таблицы ------------------------------- observe({ + req(scheme()) # проверка инициализированы ли для этой схемы наблюдатели для кнопок вложенных таблиц is_observer_is_started <- (isolate(scheme()) %in% isolate(observers_started())) @@ -746,6 +804,7 @@ server <- function(input, output, session) { ## добавить новый главный ключ ------------------------ ### modal ------- observeEvent(input$add_new_main_key_button, { + req(main_form_is_empty() != "empty") # данные для главного ключа scheme_for_key_input <- mhcs()$get_scheme("main") |> @@ -797,6 +856,8 @@ server <- function(input, output, session) { ## переход на главный акран ----------------------- ### show modal ------- observeEvent(input$clean_data_button, { + req(main_form_is_empty() == "form") + showModal(modalDialog( "Данное действие очистит все заполненные данные. Убедитесь, что нужные данные сохранены.", title = "Очистить форму?", @@ -814,7 +875,7 @@ server <- function(input, output, session) { # rewrite all inputs with empty data values$main_key <- NULL utils$clean_forms("main", mhcs()) - main_form_is_empty(TRUE) + main_form_is_empty("main_menu") removeModal() showNotification("Данные очищены!", type = "warning") @@ -844,6 +905,7 @@ server <- function(input, output, session) { ## загрузка данных ------------------- ### modal with keys ----- observeEvent(input$load_data_button, { + req(main_form_is_empty() != "empty") con <- db$make_db_connection(scheme(),"load_data_button") on.exit(db$close_db_connection(con, "load_data_button")) @@ -925,7 +987,7 @@ server <- function(input, output, session) { } - main_form_is_empty(FALSE) + main_form_is_empty("form") } @@ -940,6 +1002,7 @@ server <- function(input, output, session) { paste0(isolate(scheme()), "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".xlsx") }, content = function(file) { + req(main_form_is_empty() != "empty") con <- db$make_db_connection(isolate(scheme()),"downloadData") on.exit(db$close_db_connection(con, "downloadData"), add = TRUE) @@ -1009,6 +1072,8 @@ server <- function(input, output, session) { paste0(values$main_key, "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".docx") }, content = function(file) { + req(main_form_is_empty() != "empty") + # prepare YAML sections empty_vec <- c( "---", @@ -1018,7 +1083,6 @@ server <- function(input, output, session) { "---", "\n" ) - box::use(modules/data_manipulations[is_this_empty_value]) # iterate by scheme parts purrr::walk( diff --git a/utils/init_login_db.r b/utils/init_login_db.r index b37760c..d91040b 100644 --- a/utils/init_login_db.r +++ b/utils/init_login_db.r @@ -3,10 +3,11 @@ # SETUP AUTH ============================= # Init DB using credentials data credentials <- data.frame( - user = c("admin", "user"), - password = c("admin", "user"), + user = c("admin", "user", "user2"), + password = c("admin", "user", "user2"), # password will automatically be hashed - admin = c(TRUE, FALSE), + admin = c(TRUE, FALSE, FALSE), + scheme_access = c(NA, "all", "example_of_scheme"), # NA - none | all - all | string with seperate stringsAsFactors = FALSE ) From eb11ad86728df8770329e6d6f88ca33a6060807f Mon Sep 17 00:00:00 2001 From: madeliri Date: Sat, 6 Jun 2026 14:10:09 +0300 Subject: [PATCH 3/6] config update --- descr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/descr.yml b/descr.yml index 7f7cbce..60bb454 100644 --- a/descr.yml +++ b/descr.yml @@ -1,4 +1,4 @@ default: - form_app_version: 0.17.0 - form_id: new_formy - form_name: NEW FORMY \ No newline at end of file + form_app_version: 0.18.0 + form_id: formy + form_name: FORMY \ No newline at end of file From 358a238f4e527ccc3de12a74e48e2246bf66a175 Mon Sep 17 00:00:00 2001 From: madeliri Date: Mon, 8 Jun 2026 21:53:15 +0300 Subject: [PATCH 4/6] =?UTF-8?q?0.18.1=20=20(fix=20-=20=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=20=D0=B8=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ README.md | 1 + app.R | 10 ++++++---- descr.yml | 2 +- modules/db.R | 5 +++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5f9a6..5a3a7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 0.18.1 (2026-06-08) +##### fix +- правильный экспорт текстовых данных + ### 0.18.0 (2026-06-06) ##### features - возможность отрганичить доступ к базам данных для отдельных пользователей diff --git a/README.md b/README.md index ac317ba..6e402b3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ git clone https://gitea.madelirihs.ru/madeliri/shiny_form.git Восстановление окружения ```r +renv::activate() renv::init() ``` diff --git a/app.R b/app.R index f0b8b3b..d326593 100644 --- a/app.R +++ b/app.R @@ -1020,7 +1020,8 @@ server <- function(input, output, session) { date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) number_columns <- subset(scheme, form_type == "number", form_id, drop = TRUE) - other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + # other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + other_cols <- colnames(df)[!(colnames(df) %in% c(date_columns, number_columns))] df <- df |> dplyr::mutate( @@ -1028,7 +1029,7 @@ server <- function(input, output, session) { dplyr::across(tidyselect::all_of({{date_columns}}), as.Date), # числа - к единому формату десятичных значений dplyr::across(tidyselect::all_of({{number_columns}}), ~ gsub("\\.", "," , .x)), - dplyr::across(tidyselect::all_of({{other_cols}}), as.character) + dplyr::across(tidyselect::all_of({{other_cols}}), \(x) dplyr::if_else(x == "", as.character(NA), as.character(x))) ) |> # очистка от пустых ключей dplyr::filter(!is.na(mhcs()$get_main_key_id)) @@ -1238,7 +1239,8 @@ server <- function(input, output, session) { date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) number_columns <- subset(scheme, form_type == "number", form_id, drop = TRUE) - other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + # other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + other_cols <- colnames(df)[!(colnames(df) %in% c(date_columns, number_columns))] # функция для преобразование числовых значений и сохранения "NA" num_converter <- function(old_col) { @@ -1258,7 +1260,7 @@ server <- function(input, output, session) { # даты - к единому формату dplyr::across(tidyselect::all_of({{date_columns}}), \(x) purrr::map_chr(x, db$excel_to_db_dates_converter)), dplyr::across(tidyselect::all_of({{number_columns}}), num_converter), - dplyr::across(tidyselect::all_of({{other_cols}}), as.character) + dplyr::across(tidyselect::all_of({{other_cols}}), \(x) dplyr::if_else(x == "", as.character(NA), as.character(x))) ) |> select(all_of(unique(c(main_key_id, scheme$form_id)))) diff --git a/descr.yml b/descr.yml index 60bb454..a95c37f 100644 --- a/descr.yml +++ b/descr.yml @@ -1,4 +1,4 @@ default: - form_app_version: 0.18.0 + form_app_version: 0.18.1 form_id: formy form_name: FORMY \ No newline at end of file diff --git a/modules/db.R b/modules/db.R index bc83242..1d8f0c5 100644 --- a/modules/db.R +++ b/modules/db.R @@ -200,7 +200,8 @@ write_df_to_db = function( date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) number_columns <- subset(scheme, form_type == "number", form_id, drop = TRUE) - other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + # other_cols <- which(colnames(df) %in% c(date_columns, number_columns)) + other_cols <- colnames(df)[!(colnames(df) %in% c(date_columns, number_columns))] df <- df |> dplyr::mutate( @@ -208,7 +209,7 @@ write_df_to_db = function( dplyr::across(tidyselect::all_of({{date_columns}}), \(x) purrr::map_chr(x, excel_to_db_dates_converter)), # числа - к единому формату десятичных значений dplyr::across(tidyselect::all_of({{number_columns}}), ~ gsub("\\.", "," , .x)), - dplyr::across(tidyselect::all_of({{other_cols}}), as.character), + dplyr::across(tidyselect::all_of({{other_cols}}), \(x) dplyr::if_else(x == "", as.character(NA), as.character(x))) ) if (table_name == "main") { From 835f0535848d4180cd2ffca584832a8b4cc12649 Mon Sep 17 00:00:00 2001 From: madeliri Date: Sat, 13 Jun 2026 17:21:04 +0300 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D1=88=D0=B8=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + app.R | 16 ++++++++-------- app/logs.R | 21 +++++++++++++++++++++ app/tasks.R | 3 --- modules/global_options.R | 5 +++++ test.xlsx | Bin 0 -> 8848 bytes 6 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 app/logs.R create mode 100644 test.xlsx diff --git a/README.md b/README.md index 6e402b3..f3bf67e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ FORM_APP_LOCAL_DB_BACKUP_PATH="path_to_backups" Проверка осуществляется при каждом запуске приложения, бэкапы создаются раз в день (при первом запуске). Количество послдних сохраненных бэкапов: + ``` FORM_APP_LOCAL_DB_BACKUP_LIMITS=3 ``` diff --git a/app.R b/app.R index d326593..14d5aac 100644 --- a/app.R +++ b/app.R @@ -175,7 +175,6 @@ server <- function(input, output, session) { } }) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # REACTIVE VALUES ================================= # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -253,7 +252,6 @@ server <- function(input, output, session) { hr(), "Для начала работы нужно создать новую запись или загрузить существующую!", hr(), - # сво # загрузка панели для работы с базой данных uiOutput("admin_buttons_panel") ) @@ -280,6 +278,7 @@ server <- function(input, output, session) { output$base_data <- renderUI({ if (main_form_is_empty() == "main_menu") { + con <- db$make_db_connection(scheme(),"base_data") on.exit(db$close_db_connection(con, "base_data"), add = TRUE) @@ -292,13 +291,13 @@ server <- function(input, output, session) { # задачи на сегодня if ("tasks" %in% DBI::dbListTables(con)) { - tasks_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM tasks WHERE task_status = 'active'")) |> + tasks_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM \"tasks\" WHERE task_status = 'active'")) |> dplyr::pull() - tasks_today_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM tasks WHERE task_status = 'active' AND task_due_date = {as.integer(Sys.Date())}")) |> + tasks_today_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM \"tasks\" WHERE task_status = 'active' AND task_due_date = {as.integer(Sys.Date())}")) |> dplyr::pull() - tasks_overdue_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM tasks WHERE task_status = 'active' AND task_due_date < {as.integer(Sys.Date())}")) |> + tasks_overdue_count <- DBI::dbGetQuery(con, glue::glue("SELECT COUNT (task_id) FROM \"tasks\" WHERE task_status = 'active' AND task_due_date < {as.integer(Sys.Date())}")) |> dplyr::pull() } else { @@ -709,10 +708,11 @@ server <- function(input, output, session) { # загрузка данных в формы forms$load_data_to_form( df = df, - table_name = values$nested_form_id, - mhcs = mhcs, + table_name = values$nested_form_id, + mhcs = mhcs, ns = NS(values$nested_form_id) ) + } else { utils$clean_forms(values$nested_form_id, mhcs(), NS(values$nested_form_id)) } @@ -1258,7 +1258,7 @@ server <- function(input, output, session) { df <- df |> dplyr::mutate( # даты - к единому формату - dplyr::across(tidyselect::all_of({{date_columns}}), \(x) purrr::map_chr(x, db$excel_to_db_dates_converter)), + dplyr::across(tidyselect::all_of({{date_columns}}), \(x) purrr::map_chr(x, db$excel_to_db_dates_converter)), dplyr::across(tidyselect::all_of({{number_columns}}), num_converter), dplyr::across(tidyselect::all_of({{other_cols}}), \(x) dplyr::if_else(x == "", as.character(NA), as.character(x))) ) |> diff --git a/app/logs.R b/app/logs.R new file mode 100644 index 0000000..7745a0d --- /dev/null +++ b/app/logs.R @@ -0,0 +1,21 @@ +box::use( + shiny[...], + bslib[...] +) + +options(box.path = here::here()) +box::use( + modules/db, + modules/utils, + app/forms +) + +#' @export +server <- function(id, values, scheme, mhcs) { + + ns <- NS(id) + + moduleServer(id, function(input, output, session) { + + }) +} diff --git a/app/tasks.R b/app/tasks.R index c6749e9..4d50fc7 100644 --- a/app/tasks.R +++ b/app/tasks.R @@ -1,4 +1,3 @@ - box::use( shiny[...], bslib[...] @@ -438,13 +437,11 @@ update_task_button_count <- function(con, values, ns) { inputID <- "display_task_modal" if (!missing(ns)) inputID <- ns(inputID) - # если ключ не определен - выход из функции if (is.null(values$main_key)) { updateActionButton(inputId = inputID, label = "Задачи") return() - } # при наличии таблицы - полу diff --git a/modules/global_options.R b/modules/global_options.R index 96231c1..dc94b7c 100644 --- a/modules/global_options.R +++ b/modules/global_options.R @@ -54,6 +54,11 @@ check_and_init_scheme = function() { "modules/utils.R" ) + # проверка существования отслеживаемых файлов + if (!all(file.exists(files_to_watch))) { + cli::cli_abort("проверка схем: {files_to_watch[!file.exists(files_to_watch)]} is not exists") + } + scheme_names <- names(config::get()$form_schemes) scheme_file <- paste0(config::get("form_app_configure_path"), "/schemas/", scheme_names, ".xlsx") scheme_file <- stats::setNames(scheme_file, scheme_names) diff --git a/test.xlsx b/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dc38ed350eb60ac5229beeb10d023c6b4954b80e GIT binary patch literal 8848 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 A@c;k- literal 0 HcmV?d00001 From 0c9dda215d6eeb4f83cf7875e7f7284f57f8e20f Mon Sep 17 00:00:00 2001 From: madeliri Date: Sat, 13 Jun 2026 18:26:53 +0300 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D0=B9=20=D0=BF=D0=BE=20=D0=B1=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.R | 12 +++++- app/logs.R | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ app/tasks.R | 1 - modules/db.R | 1 + 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/app.R b/app.R index 14d5aac..0334fca 100644 --- a/app.R +++ b/app.R @@ -18,6 +18,7 @@ box::use( modules/data_validation, app/forms, app/tasks, + app/logs, modules/data_manipulations[is_this_empty_value] ) @@ -65,8 +66,8 @@ ui <- page_sidebar( # downloadButton("downloadDocx", "get .docx (test only)"), uiOutput("status_message"), textOutput("status_message2"), - uiOutput("display_log"), actionButton("tasks-display_task_modal", "Задачи: нет активных", icon("list-check")), + uiOutput("logs-display_log"), position = "left", open = list(mobile = "always"), popover( @@ -164,12 +165,16 @@ server <- function(input, output, session) { if (showing_buttons) { tagList( - br(), strong("Импорт и экспорт данных для выбранной схемы:"), verticalLayout( downloadButton("downloadData", "Экспорт в .xlsx", style = "width: 250px; margin-top: 5px"), actionButton("button_upload_data_from_xlsx", "импорт!", icon("file-import", lib = "font-awesome"), style = "width: 250px; margin-top: 10px"), fluid = FALSE + ), + strong("Дополнительные опции:"), + verticalLayout( + actionButton("logs-show_last_actions", "Все действия", icon("scroll", lib = "font-awesome"), style = "width: 250px; margin-top: 10px"), + fluid = FALSE ) ) } @@ -1363,6 +1368,9 @@ server <- function(input, output, session) { # TASKS --------------------------------------- tasks$server("tasks", values, scheme, mhcs) + # SHOW LOGS ----------------------------------- + logs$server("logs", values, scheme, mhcs) + } diff --git a/app/logs.R b/app/logs.R index 7745a0d..c8ec345 100644 --- a/app/logs.R +++ b/app/logs.R @@ -17,5 +17,113 @@ server <- function(id, values, scheme, mhcs) { moduleServer(id, function(input, output, session) { + # отображение DT-таблицы со списком последних действий + observeEvent(input$show_last_actions, { + + con <- db$make_db_connection(scheme(),"show_last_actions") + on.exit(db$close_db_connection(con, "show_last_actions"), add = TRUE) + + log_df <- DBI::dbReadTable(con, "log") + + # новые записи вначале, формат даты с временем + log_df <- log_df |> + dplyr::arrange(dplyr::desc(date)) |> + dplyr::mutate(date = format(as.POSIXct(date), "%d.%m.%Y %H:%M")) + + output$dt_logs <- DT::renderDataTable( + DT::datatable( + log_df, + # caption = 'Table 1: This is a simple caption for the table.', + rownames = FALSE, + colnames = logs_colnames, + extensions = c('KeyTable', "FixedColumns"), + # editable = 'cell', + class = 'cell-border stripe', + selection = "single", + options = list( + # dom = 'tipf', + scrollX = TRUE, + fixedColumns = list(leftColumns = 1), + keys = TRUE, + autoWidth = TRUE, + columnDefs = list( + list(width = "150px", targets = c(0,6)), + list(width = "110px", targets = c(1:4)) + ) + ) + ) + ) + + showModal(modalDialog( + DT::dataTableOutput(ns("dt_logs")), + size = "xl", + # footer = tagList( + # actionButton("nested_form_dt_save", "сохранить изменения") + # ), + easyClose = TRUE + )) + + }) + + # observe({ + # print(input$dt_logs_rows_selected) + # }) + + output$display_log <- renderUI({ + req(values$main_key) + + # получение логов + con <- db$make_db_connection(scheme(),"display_log") + on.exit(db$close_db_connection(con, "display_log"), add = TRUE) + + query <- sprintf("SELECT * FROM \"log\" WHERE key = '%s'", values$main_key) + log_df_for_id <- DBI::dbGetQuery(con, query) + + if (nrow(log_df_for_id) > 0) { + + lines <- log_df_for_id |> + dplyr::mutate( + date = as.POSIXct(date), + date = date + lubridate::hours(3), # fix datetime + date_day = as.Date(date) + ) |> + dplyr::mutate(cons_actions = dplyr::consecutive_id(action, user)) |> + dplyr::mutate(n_actions = dplyr::row_number(), .by = c(cons_actions, user, action, date_day)) |> + dplyr::slice(which.max(n_actions), .by = c(user, action, date_day)) |> + dplyr::arrange(date) |> + dplyr::mutate(string_to_print = sprintf( + "[%s %s] %s: %s (%s)", + format(date, "%d.%m.%y"), + format(date, "%H:%M"), + user, + action, + n_actions + )) |> + dplyr::pull(string_to_print) |> + paste(collapse = "
") + + } else { + lines <- "" + } + + div( + strong("Последние действия:"), + br(), + HTML(lines), + style = "font-size:10px;" + ) + + }) + }) } + +logs_colnames <- c( + "время" = "date", + "пользователь" = "user", + "приложение" = "app_id", + "версия" = "app_ver", + "ip" = "remote_addr", + "ID записи" = "key", + "действие" = "action" +) diff --git a/app/tasks.R b/app/tasks.R index 4d50fc7..998fca2 100644 --- a/app/tasks.R +++ b/app/tasks.R @@ -238,7 +238,6 @@ server <- function(id, values, scheme, mhcs) { date_cols <- c("task_datetime_created", "task_datetime_completed", "task_datetime_last_updated", "task_due_date") date_cols <- which(colnames(values$tasks_data) %in% date_cols) - output$dt_tasks <- DT::renderDataTable( DT::datatable( values$tasks_data, diff --git a/modules/db.R b/modules/db.R index 1d8f0c5..67bc71c 100644 --- a/modules/db.R +++ b/modules/db.R @@ -10,6 +10,7 @@ make_db_connection = function(scheme, where = "") { scheme, ext = "sqlite" )) + } #' @export