diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3a7b7..0e719d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### 0.18.2 - 0.18.3 (2026-06-18) +##### features +- возможность отражение лога действий по базам (для администраторов) и отдельно для каждой записи (сгруппированы по уникальным действиям) +- возможность экспорта таблицы со списком значений не прошедшие валидацию согласно схеме (для администраторов) + +##### refactor +- перестройка структуры репозитория + ### 0.18.1 (2026-06-08) ##### fix - правильный экспорт текстовых данных diff --git a/R/modules/data_validation.R b/R/modules/data_validation.R index 146ed7e..fc8d703 100644 --- a/R/modules/data_validation.R +++ b/R/modules/data_validation.R @@ -1,5 +1,7 @@ options(box.path = here::here()) -box::use(R/modules/data_manipulations[is_this_empty_value]) +box::use( + R/modules/data_manipulations[is_this_empty_value] +) #' @export init_val = function(scheme, ns) { @@ -114,4 +116,134 @@ val_choice_within_a_dict = function(x, choices) { text <- paste0("'",x[!compare_to_dict],"'", collapse = ", ") glue::glue("варианты, не соответствующие схеме: {text}") } -} \ No newline at end of file +} + +# ЭКСПОРТ ДАННЫХ ДЛЯ ВАЛИДАЦИИ +#' @export +validate_value_for_form <- function(data_to_check, form_type, choices, val_required) { + + res <- NULL + # for `number` type: if in `choices` column has values then parsing them to range validation + # value `0; 250` -> transform to rule validation value from 0 to 250 + if (form_type == "number") { + + res <- val_is_a_number(data_to_check) + if(!is.null(res)) return(res) + + # проверка на соответствие диапазону значений + if (!is.na(choices)) { + + # разделить на несколько елементов + ranges <- as.integer(stringr::str_split_1(choices, "; ")) + + # проверка на кол-во значений + if (length(ranges) > 3) { + warning("Количество переданных элементов'", x_input_id, "' > 2") + } else { + + res <- val_number_within_a_range(data_to_check, ranges = ranges) + if (!is.null(res)) return(res) + + } + } + } + + if (form_type %in% c("select_multiple", "select_one", "radio", "checkbox")) { + + if (!is_this_empty_value(data_to_check)) { + split_data <- stringr::str_split_1(data_to_check, "; ") + } else { + split_data <- NA + } + + res <- val_choice_within_a_dict(split_data, choices = choices) + if(!is.null(res)) return(res) + } + + # if in `required` column value is `1` apply standart validation + if (!is.na(val_required) && val_required == 1) { + + if (is_this_empty_value(data_to_check)) { + + res <- "Необходимо заполнить." + if(!is.null(res)) return(res) + + } + + } + + if(is.null(res)) return(NA) +} + +#' @export +get_table_with_data_validation_info <- function(schm, con) { + + # итерациям по всем таблицам + purrr::map( + purrr::set_names(schm$all_tables_names), + .f = \(table_name) { + + scheme <- schm$get_scheme(table_name) + data <- DBI::dbReadTable(con, table_name) + + inputs_simple_list <- schm$get_id_type_list(table_name) + + main_key <- schm$get_main_key_id + key <- schm$get_key_id(table_name) + + # итерация по всем form_id в текущей таблице + ff <- purrr::map2( + .x = names(inputs_simple_list), + .y = unname(inputs_simple_list), + .f = \(x_input_id, y_form_type) { + + this_id_scheme <- dplyr::filter(scheme, form_id == {{x_input_id}}) + + choices <- this_id_scheme$choices + val_required <- unique(this_id_scheme$required) + + # extract data + data_to_check <- data[[x_input_id]] + main_keys <- data[[main_key]] + keys <- data[[key]] + iter <- purrr::set_names(data_to_check, keys) + + # cli::cli_inform("~ input_id: {x_input_id} | type: {form_type} | value: {data_to_check}") + validation_info <- purrr::map(iter, validate_value_for_form, y_form_type, choices, val_required) + + df_with_result_and_validation_for_id <- tibble::enframe(validation_info, name = key, value = "value") |> + tidyr::unnest(cols = c(value)) + + # если главный ключ и ключ для таблицы не одно и тоже, добавление главного ключа в таблицу + if (main_key != key) { + df_with_result_and_validation_for_id[main_key] <- main_keys + } + + df_with_result_and_validation_for_id |> + dplyr::mutate(form_id = x_input_id) + + }) |> + purrr::list_rbind() + + ff |> + dplyr::distinct() |> + dplyr::filter(!is.na(value)) |> + # данные схемы для 'читабельности информации' + dplyr::left_join( + y = scheme |> dplyr::distinct(form_id, form_label) |> dplyr::mutate(nr = dplyr::row_number()), + by = dplyr::join_by(form_id), + ) |> + # сортировка (main_key, затем id формы - по порядку в схеме) + dplyr::arrange(!!rlang::sym(main_key), !!rlang::sym(main_key), nr) |> + dplyr::select( + !!rlang::sym(main_key), !!rlang::sym(main_key), + form_id, + form_label, + value + ) + + } + ) + +} + diff --git a/app.R b/app.R index 90b7f2e..b7a5391 100644 --- a/app.R +++ b/app.R @@ -157,6 +157,7 @@ server <- function(input, output, session) { strong("Дополнительные опции:"), verticalLayout( actionButton("logs-show_last_actions", "Все действия", icon("scroll", lib = "font-awesome"), style = "width: 250px; margin-top: 10px"), + downloadButton("download_data_validation_info", "Некорректно заполненные данные (.xlsx)", icon("scroll", lib = "font-awesome"), style = "width: 250px; margin-top: 10px"), fluid = FALSE ) ) @@ -1279,7 +1280,7 @@ server <- function(input, output, session) { keys_to_delete <- all_existed_keys[all_existed_keys %in% all_new_keys] keys_to_delete <- paste0("'", keys_to_delete, "'", collapse = ", ") - DBI::dbExecute(connection(), glue::glue("DELETE FROM {table_name} WHERE \"{main_key_id}\" IN ({keys_to_delete})")) + DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE \"{main_key_id}\" IN ({keys_to_delete})")) } @@ -1336,6 +1337,7 @@ server <- function(input, output, session) { "loading data", "creating new key", "exporting data to xlsx", + "export validation dataset", "importing data from xlsx" ), key = NA, @@ -1363,6 +1365,52 @@ server <- function(input, output, session) { # SHOW LOGS ----------------------------------- logs$server("logs", values, scheme, mhcs) + # экспорт таблицы с информации о валидации данных ------------------- + output$download_data_validation_info <- downloadHandler( + filename = function(){ + paste0("dvinfo_", isolate(scheme()), "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".xlsx") + }, + content = function(file) { + req(main_form_is_empty() != "empty") + + box::use( + R/modules/data_validation[get_table_with_data_validation_info] + ) + + con <- db$make_db_connection(isolate(scheme()),"download_data_validation_info") + on.exit(db$close_db_connection(con, "download_data_validation_info"), add = TRUE) + + list_of_df <- get_table_with_data_validation_info(mhcs(), con) + + # добавить мета информацию + list_of_df[["meta"]] <- dplyr::tribble( + ~`Параметр` , ~`Значение`, + "Пользователь" , values$current_user, + "Название базы" , names(ENABLED_SCHEMES)[ENABLED_SCHEMES == scheme()], + "id базы" , scheme(), + "id формы" , config::get("form_id"), + "ver формы" , config::get("form_app_version"), + "Время выгрузки" , format(Sys.time(), "%d.%m.%Y %H:%M:%S"), + ) + + # set date params + options("openxlsx2.dateFormat" = "dd.mm.yyyy") + + cli::cli_alert_success("Данные успешно экспортированы") + showNotification("Данные успешно экспортированы", type = "message") + log_action_to_db("export validation dataset", con = con) + + # pass tables to export + openxlsx2::write_xlsx( + purrr::compact(list_of_df), + file, + na.strings = "", + as_table = TRUE, + col_widths = 20 + ) + } + ) + } app <- shiny::shinyApp(ui = ui, server = server) diff --git a/config/descr.yml b/config/descr.yml index 23dc0cc..e8b51b6 100644 --- a/config/descr.yml +++ b/config/descr.yml @@ -1,4 +1,4 @@ default: - form_app_version: 0.18.2 + form_app_version: 0.18.3 form_id: formy form_name: FORMY \ No newline at end of file