Compare commits

...

17 Commits

Author SHA1 Message Date
8f3343a103 0.18.3 2026-06-18 14:02:22 +03:00
4baec1003a 0.18.2 2026-06-18 10:27:41 +03:00
7ed3ee0480 fix: path to auth db 2026-06-18 08:07:08 +03:00
a178480912 refactor: moving config files to separate folder 2026-06-17 16:54:58 +03:00
a4d829e3dd fix: more namespacing fixes 2026-06-17 14:38:26 +03:00
89d58c8df5 fix: missing namespaces for functions 2026-06-17 13:45:43 +03:00
4c50473c7e refactor: push all global vars to globla options 2026-06-16 15:40:46 +03:00
df740c4f71 fix; some comments 2026-06-15 16:44:02 +03:00
d73699a1d1 refactor: инициация схемы - при загрузке модуля, вместо загрузки пакетов через library: box and namespacing 2026-06-15 16:40:26 +03:00
e6e15392c3 refactor: вся основа кода в отдельной папке 2026-06-15 16:24:42 +03:00
2ececc8029 refactor: прячем временные файлы в отдельную папку 2026-06-15 16:18:56 +03:00
a0340d78e5 Merge branch 'main' of https://gitea.madelirihs.ru/madeliri/shiny_form 2026-06-15 16:11:31 +03:00
81dc89cf02 merge 2026-06-15 16:11:27 +03:00
0c9dda215d feat: возможность отражения информации действий по базам данных 2026-06-13 18:26:53 +03:00
835f053584 refactor: небольшие изменения 2026-06-13 17:21:04 +03:00
358a238f4e 0.18.1 (fix - корректный экспорт и импорт текстовых данных) 2026-06-08 21:53:15 +03:00
eb11ad8672 config update 2026-06-06 14:10:09 +03:00
20 changed files with 615 additions and 248 deletions

View File

@@ -28,6 +28,7 @@ source("renv/activate.R")
cli::cli_code(paste0("R_CONFIG_ACTIVE", "=")) cli::cli_code(paste0("R_CONFIG_ACTIVE", "="))
} }
Sys.setenv(R_CONFIG_FILE = "config/config.yml")
})() })()
@@ -35,8 +36,8 @@ source("renv/activate.R")
# при первом запуске скопировать пример конфига # при первом запуске скопировать пример конфига
(function() { (function() {
if (!file.exists("config.yml")) { if (!file.exists("config/config.yml")) {
file.copy("config_example.yml", "config.yml") file.copy("config/config_example.yml", "config/config.yml")
} }
})() })()

4
.gitignore vendored
View File

@@ -1,9 +1,9 @@
/renv /renv
/temp /temp
/_devel /_devel
/all_bases
scheme.rds config/config.yml
config.yml
.Renviron .Renviron
.DS_Store .DS_Store

View File

@@ -1,3 +1,15 @@
### 0.18.2 - 0.18.3 (2026-06-18)
##### features
- возможность отражение лога действий по базам (для администраторов) и отдельно для каждой записи (сгруппированы по уникальным действиям)
- возможность экспорта таблицы со списком значений не прошедшие валидацию согласно схеме (для администраторов)
##### refactor
- перестройка структуры репозитория
### 0.18.1 (2026-06-08)
##### fix
- правильный экспорт текстовых данных
### 0.18.0 (2026-06-06) ### 0.18.0 (2026-06-06)
##### features ##### features
- возможность отрганичить доступ к базам данных для отдельных пользователей - возможность отрганичить доступ к базам данных для отдельных пользователей

View File

@@ -1,6 +1,6 @@
options(box.path = here::here()) options(box.path = here::here())
box::use( box::use(
modules/utils, R/modules/utils,
) )
#' @export #' @export

129
R/app/logs.R Normal file
View File

@@ -0,0 +1,129 @@
box::use(
shiny[...],
bslib[...]
)
options(box.path = here::here())
box::use(
R/modules/db,
R/modules/utils,
R/app/forms
)
#' @export
server <- function(id, values, scheme, mhcs) {
ns <- NS(id)
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(
"<b>[%s %s]</b> %s: %s (%s)",
format(date, "%d.%m.%y"),
format(date, "%H:%M"),
user,
action,
n_actions
)) |>
dplyr::pull(string_to_print) |>
paste(collapse = "</br>")
} 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"
)

View File

@@ -1,4 +1,3 @@
box::use( box::use(
shiny[...], shiny[...],
bslib[...] bslib[...]
@@ -6,9 +5,9 @@ box::use(
options(box.path = here::here()) options(box.path = here::here())
box::use( box::use(
modules/db, R/modules/db,
modules/utils, R/modules/utils,
app/forms R/app/forms
) )
#' @export #' @export
@@ -239,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 <- c("task_datetime_created", "task_datetime_completed", "task_datetime_last_updated", "task_due_date")
date_cols <- which(colnames(values$tasks_data) %in% date_cols) date_cols <- which(colnames(values$tasks_data) %in% date_cols)
output$dt_tasks <- DT::renderDataTable( output$dt_tasks <- DT::renderDataTable(
DT::datatable( DT::datatable(
values$tasks_data, values$tasks_data,
@@ -438,13 +436,11 @@ update_task_button_count <- function(con, values, ns) {
inputID <- "display_task_modal" inputID <- "display_task_modal"
if (!missing(ns)) inputID <- ns(inputID) if (!missing(ns)) inputID <- ns(inputID)
# если ключ не определен - выход из функции # если ключ не определен - выход из функции
if (is.null(values$main_key)) { if (is.null(values$main_key)) {
updateActionButton(inputId = inputID, label = "Задачи") updateActionButton(inputId = inputID, label = "Задачи")
return() return()
} }
# при наличии таблицы - полу # при наличии таблицы - полу

249
R/modules/data_validation.R Normal file
View File

@@ -0,0 +1,249 @@
options(box.path = here::here())
box::use(
R/modules/data_manipulations[is_this_empty_value]
)
#' @export
init_val = function(scheme, ns) {
iv <- shinyvalidate::InputValidator$new()
# если передана функция с пространством имен, то происходит модификация id
if (!missing(ns)) {
scheme <- scheme |>
dplyr::mutate(form_id = ns(form_id))
}
# формируем список id - тип
inputs_simple_list <- scheme |>
dplyr::filter(!form_type %in% c("nested_forms", "description", "description_header")) |>
dplyr::distinct(form_id, form_type) |>
tibble::deframe()
# add rules to all inputs
purrr::walk(
.x = names(inputs_simple_list),
.f = \(x_input_id) {
form_type <- inputs_simple_list[[x_input_id]]
choices <- dplyr::filter(scheme, form_id == {{x_input_id}}) |>
dplyr::pull(choices)
val_required <- dplyr::filter(scheme, form_id == {{x_input_id}}) |>
dplyr::distinct(required) |>
dplyr::pull(required)
# 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") {
iv$add_rule(x_input_id, val_is_a_number)
# проверка на соответствие диапазону значений
if (!is.na(choices)) {
# разделить на несколько елементов
ranges <- as.integer(stringr::str_split_1(choices, "; "))
# проверка на кол-во значений
if (length(ranges) > 3) {
warning("Количество переданных элементов'", x_input_id, "' > 2")
} else {
iv$add_rule(x_input_id, val_number_within_a_range, ranges = ranges)
}
}
}
if (form_type %in% c("select_multiple", "select_one", "radio", "checkbox")) {
iv$add_rule(x_input_id, val_choice_within_a_dict, choices = choices)
}
# if in `required` column value is `1` apply standart validation
if (!is.na(val_required) && val_required == 1) {
iv$add_rule(x_input_id, shinyvalidate::sv_required(message = "Необходимо заполнить."))
}
}
)
iv
}
# работа с числовыми значениями ------------------
## проверка является ли значение числом ----------
val_is_a_number = function(x) {
# exit if empty
if (is_this_empty_value(x)) return(NULL)
# хак для пропуска значений
if (x == "NA") return(NULL)
# check for numeric
# if (grepl("^[-]?(\\d*\\,\\d+|\\d+\\,\\d*|\\d+)$", x)) NULL else "Значение должно быть числом."
if (grepl("^[+-]?\\d*[\\.|\\,]?\\d+$", x)) NULL else "Значение должно быть числом."
}
## находится ли число в заданном диапазоне значений -------
val_number_within_a_range = function(x, ranges) {
# exit if empty
if (is_this_empty_value(x)) return(NULL)
if (x == "NA") return(NULL)
# замена разделителя десятичных цифр
x <- stringr::str_replace(x, ",", ".")
# check for currect value
if (dplyr::between(as.double(x), ranges[1], ranges[2])) {
NULL
} else {
glue::glue("Значение должно быть между {ranges[1]} и {ranges[2]}.")
}
}
# списки ---------------------------------------------------------
## являются ли выбранные значения допустимы (согласно файлу схемы)
val_choice_within_a_dict = function(x, choices) {
if (length(x) == 1) {
if (is_this_empty_value(x)) return(NULL)
}
# проверка на соответствие вариантов схеме ---------
compare_to_dict <- (x %in% choices)
if (!all(compare_to_dict)) {
text <- paste0("'",x[!compare_to_dict],"'", collapse = ", ")
glue::glue("варианты, не соответствующие схеме: {text}")
}
}
# ЭКСПОРТ ДАННЫХ ДЛЯ ВАЛИДАЦИИ
#' @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
)
}
)
}

View File

@@ -10,6 +10,7 @@ make_db_connection = function(scheme, where = "") {
scheme, scheme,
ext = "sqlite" ext = "sqlite"
)) ))
} }
#' @export #' @export
@@ -102,7 +103,7 @@ get_dummy_data = function(type) {
get_dummy_df = function(forms_id_type_list) { get_dummy_df = function(forms_id_type_list) {
options(box.path = here::here()) options(box.path = here::here())
box::use(modules/utils) box::use(R/modules/utils)
purrr::map( purrr::map(
.x = forms_id_type_list, .x = forms_id_type_list,
@@ -135,7 +136,7 @@ compare_existing_table_with_schema = function(
} }
options(box.path = here::here()) options(box.path = here::here())
box::use(modules/utils) box::use(R/modules/utils)
# checking if db structure in form compatible with alrady writed data (in case on changig form) # checking if db structure in form compatible with alrady writed data (in case on changig form)
if (identical(colnames(DBI::dbReadTable(con, table_name)), all_ids_from_schema)) { if (identical(colnames(DBI::dbReadTable(con, table_name)), all_ids_from_schema)) {
@@ -200,7 +201,8 @@ write_df_to_db = function(
date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE)
number_columns <- subset(scheme, form_type == "number", 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 |> df <- df |>
dplyr::mutate( dplyr::mutate(
@@ -208,7 +210,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({{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({{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") { if (table_name == "main") {

View File

@@ -1,10 +1,20 @@
.on_load = function(ns) {
check_and_init_scheme()
# set global settings:
set_global_options(
shiny.host = "0.0.0.0",
shiny.port = 1338,
APP.DEBUG = FALSE
)
}
#' @export #' @export
#' @description костыли для упрощения работы себе #' @description костыли для упрощения работы себе
set_global_options = function( set_global_options = function(
SYMBOL_DELIM = "; ", SYMBOL_DELIM = "; ",
APP.DEBUG = FALSE,
shiny.host = "127.0.0.1",
shiny.port = 1338,
... ...
) { ) {
@@ -27,33 +37,43 @@ set_global_options = function(
options( options(
SYMBOL_DELIM = SYMBOL_DELIM, SYMBOL_DELIM = SYMBOL_DELIM,
# form.db_path = config::get("form_db_path"),
APP.DEBUG = APP.DEBUG,
# APP.FILE_DB = APP.FILE_DB,
shiny.host = shiny.host,
shiny.port = shiny.port,
... ...
) )
} }
# global vars ------------------------------------
#' @export #' @export
AUTH_ENABLED <- config::get("form_auth_enabled") AUTH_ENABLED <- config::get("form_auth_enabled")
#' @export
#' TODO: нормальный разворот
ENABLED_SCHEMES <- unlist(config::get()$form_schemes)
ENABLED_SCHEMES <- stats::setNames(names(ENABLED_SCHEMES), ENABLED_SCHEMES)
# -------------------------------------------------
#' @export #' @export
check_and_init_scheme = function() { check_and_init_scheme = function() {
cli::cli_inform(c("*" = "проверка схемы...")) cli::cli_inform(c("*" = "проверка схемы..."))
options(box.path = here::here()) options(box.path = here::here())
box::use(modules/db[local_db_backup]) box::use(
R/modules/db[local_db_backup]
)
# список файлов, изменение которых, приведут к переинициализиации схемы # список файлов, изменение которых, приведут к переинициализиации схемы
files_to_watch <- c( files_to_watch <- c(
"config.yml", "config/config.yml",
"modules/scheme_generator.R", "R/modules/scheme_generator.R",
"modules/utils.R" "R/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_names <- names(config::get()$form_schemes)
scheme_file <- paste0(config::get("form_app_configure_path"), "/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) scheme_file <- stats::setNames(scheme_file, scheme_names)
@@ -70,7 +90,7 @@ check_and_init_scheme = function() {
exist_hash <- tools::md5sum(c(scheme_file, files_to_watch)) exist_hash <- tools::md5sum(c(scheme_file, files_to_watch))
# если первый запуск (нет файла с кешем) инициализация схемы # если первый запуск (нет файла с кешем) инициализация схемы
if (!file.exists(hash_file) | !file.exists("scheme.rds") | !all(file.exists(db_files))) { if (!file.exists(hash_file) | !file.exists("temp/scheme.rds") | !all(file.exists(db_files))) {
init_scheme(scheme_file) init_scheme(scheme_file)
@@ -106,8 +126,8 @@ init_scheme = function(scheme_file) {
options(box.path = here::here()) options(box.path = here::here())
box::use( box::use(
modules/db, R/modules/db,
modules/scheme_generator[scheme_R6] R/modules/scheme_generator[scheme_R6]
) )
db_path <- fs::path(config::get("form_app_configure_path"), "db") db_path <- fs::path(config::get("form_app_configure_path"), "db")
@@ -150,5 +170,5 @@ init_scheme = function(scheme_file) {
cli::cli_abort(c("В одной или нескольких схемах наименования вложенных форм совпадают:", paste("-", names(tab)[tab > 1]))) cli::cli_abort(c("В одной или нескольких схемах наименования вложенных форм совпадают:", paste("-", names(tab)[tab > 1])))
} }
saveRDS(schms, "scheme.rds") saveRDS(schms, "temp/scheme.rds")
} }

View File

@@ -49,14 +49,14 @@ scheme_R6 <- R6::R6Class(
"task_status", "select_one", "Статус задачи", NA, "deleted", "task_status", "select_one", "Статус задачи", NA, "deleted",
"task_title", "text", "Название задачи", NA, NA, "task_title", "text", "Название задачи", NA, NA,
"task_description", "text", "Описание задачи", "краткое описание", "3", "task_description", "text", "Описание задачи", "краткое описание", "3",
"task_due_date", "date", "Дата выполнения задачи", NA, NA, "task_due_date", "date", "Срок выполнения задачи", NA, NA,
) |> ) |>
dplyr::mutate(condition = NA) dplyr::mutate(condition = NA)
# extract main key # extract main key
private$main_key_id <- self$get_key_id("main") private$main_key_id <- self$get_key_id("main")
box::use(modules/utils) box::use(R/modules/utils)
private$bslib_rendered_ui <- bslib::navset_card_underline( private$bslib_rendered_ui <- bslib::navset_card_underline(
id = "main", id = "main",
!!!utils$make_list_of_pages(private$schemes_list[["main"]], private$main_key_id), !!!utils$make_list_of_pages(private$schemes_list[["main"]], private$main_key_id),

View File

@@ -266,7 +266,7 @@ update_forms_with_data = function(
) { ) {
options(box.path = here::here()) options(box.path = here::here())
box::use(modules/data_manipulations[is_this_empty_value]) box::use(R/modules/data_manipulations[is_this_empty_value])
# print("-----------------") # print("-----------------")
# cli::cli_inform("form_id: {form_id} | form_type: {form_type}") # cli::cli_inform("form_id: {form_id} | form_type: {form_type}")

View File

@@ -14,7 +14,7 @@ credentials <- data.frame(
# Init the database # Init the database
shinymanager::create_db( shinymanager::create_db(
credentials_data = credentials, credentials_data = credentials,
sqlite_path = "auth.sqlite", # will be created sqlite_path = "config/auth.sqlite", # will be created
passphrase = Sys.getenv("AUTH_DB_KEY") passphrase = Sys.getenv("AUTH_DB_KEY")
# passphrase = "passphrase_wihtout_keyring" # passphrase = "passphrase_wihtout_keyring"
) )

View File

@@ -23,6 +23,7 @@ git clone https://gitea.madelirihs.ru/madeliri/shiny_form.git
Восстановление окружения Восстановление окружения
```r ```r
renv::activate()
renv::init() renv::init()
``` ```
@@ -47,6 +48,7 @@ FORM_APP_LOCAL_DB_BACKUP_PATH="path_to_backups"
Проверка осуществляется при каждом запуске приложения, бэкапы создаются раз в день (при первом запуске). Проверка осуществляется при каждом запуске приложения, бэкапы создаются раз в день (при первом запуске).
Количество послдних сохраненных бэкапов: Количество послдних сохраненных бэкапов:
``` ```
FORM_APP_LOCAL_DB_BACKUP_LIMITS=3 FORM_APP_LOCAL_DB_BACKUP_LIMITS=3
``` ```

215
app.R
View File

@@ -1,44 +1,28 @@
suppressPackageStartupMessages({
library(DBI)
library(tidyr)
library(dplyr)
library(purrr)
library(magrittr)
library(shiny)
library(bslib)
library(shinymanager)
})
# SOURCE FILES ============================ # SOURCE FILES ============================
# packages
box::purge_cache() box::purge_cache()
box::use( box::use(
modules/utils, bslib[...],
modules/global_options, shiny[...]
modules/db,
modules/data_validation,
app/forms,
app/tasks,
modules/data_manipulations[is_this_empty_value]
) )
# modules
# global settings:
global_options$set_global_options(
shiny.host = "0.0.0.0",
APP.DEBUG = FALSE
)
# init:
global_options$check_and_init_scheme()
# global vars:
box::use( box::use(
modules/global_options[AUTH_ENABLED] R/modules/utils,
R/modules/db,
R/modules/data_validation,
R/app/forms,
R/app/tasks,
R/app/logs,
R/modules/data_manipulations[is_this_empty_value]
) )
enabled_schemes <- unlist(config::get()$form_schemes)
enabled_schemes <- setNames(names(enabled_schemes), enabled_schemes)
# load schemes object: # глобальные переменные и проверка/инициация схемы:
schms <- readRDS("scheme.rds") box::use(
R/modules/global_options[AUTH_ENABLED, ENABLED_SCHEMES],
)
SCHMS <- readRDS("temp/scheme.rds")
# CHECK FOR PANDOC ---------- # CHECK FOR PANDOC ----------
# rmarkdown::find_pandoc(dir = "/opt/homebrew/bin/") # rmarkdown::find_pandoc(dir = "/opt/homebrew/bin/")
@@ -65,8 +49,8 @@ ui <- page_sidebar(
# downloadButton("downloadDocx", "get .docx (test only)"), # downloadButton("downloadDocx", "get .docx (test only)"),
uiOutput("status_message"), uiOutput("status_message"),
textOutput("status_message2"), textOutput("status_message2"),
uiOutput("display_log"),
actionButton("tasks-display_task_modal", "Задачи: нет активных", icon("list-check")), actionButton("tasks-display_task_modal", "Задачи: нет активных", icon("list-check")),
uiOutput("logs-display_log"),
position = "left", position = "left",
open = list(mobile = "always"), open = list(mobile = "always"),
popover( popover(
@@ -117,8 +101,8 @@ server <- function(input, output, session) {
res_auth <- if (AUTH_ENABLED) { res_auth <- if (AUTH_ENABLED) {
# check_credentials directly on sqlite db # check_credentials directly on sqlite db
shinymanager::secure_server( shinymanager::secure_server(
check_credentials = check_credentials( check_credentials = shinymanager::check_credentials(
db = "auth.sqlite", db = "config/auth.sqlite",
passphrase = Sys.getenv("AUTH_DB_KEY") passphrase = Sys.getenv("AUTH_DB_KEY")
), ),
keep_token = TRUE keep_token = TRUE
@@ -135,7 +119,7 @@ server <- function(input, output, session) {
forms_access <- stringr::str_split_1(string, ", ") forms_access <- stringr::str_split_1(string, ", ")
# check if exists # check if exists
exists <- forms_access %in% enabled_schemes exists <- forms_access %in% ENABLED_SCHEMES
if (!all(exists)) { if (!all(exists)) {
cli::cli_warn(c("these forms is not exist:", paste("- ", forms_access[!exists]))) cli::cli_warn(c("these forms is not exist:", paste("- ", forms_access[!exists])))
} }
@@ -164,18 +148,22 @@ server <- function(input, output, session) {
if (showing_buttons) { if (showing_buttons) {
tagList( tagList(
br(),
strong("Импорт и экспорт данных для выбранной схемы:"), strong("Импорт и экспорт данных для выбранной схемы:"),
verticalLayout( verticalLayout(
downloadButton("downloadData", "Экспорт в .xlsx", style = "width: 250px; margin-top: 5px"), 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"), actionButton("button_upload_data_from_xlsx", "Импорт базы из .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"),
downloadButton("download_data_validation_info", "Некорректно заполненные данные (.xlsx)", icon("scroll", lib = "font-awesome"), style = "width: 250px; margin-top: 10px"),
fluid = FALSE fluid = FALSE
) )
) )
} }
}) })
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# REACTIVE VALUES ================================= # REACTIVE VALUES =================================
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -189,7 +177,7 @@ server <- function(input, output, session) {
nested_form_id = NULL, nested_form_id = NULL,
tasks_id = NULL, tasks_id = NULL,
current_user = NULL, current_user = NULL,
user_form_access = enabled_schemes user_form_access = ENABLED_SCHEMES
) )
scheme <- reactiveVal(NULL) # наименование выбранной схемы scheme <- reactiveVal(NULL) # наименование выбранной схемы
@@ -217,16 +205,16 @@ server <- function(input, output, session) {
allowed_schemas <- if (is.na(res)) { allowed_schemas <- if (is.na(res)) {
NA # нет доступа NA # нет доступа
} else if (res == "all") { } else if (res == "all") {
enabled_schemes # все схемы ENABLED_SCHEMES # все схемы
} else { } else {
enabled_schemes[enabled_schemes == res] # только указанные ENABLED_SCHEMES[ENABLED_SCHEMES == res] # только указанные
} }
# переопределяем переменные # переопределяем переменные
main_form_is_empty(ifelse(is.na(res), "empty", "main_menu")) main_form_is_empty(ifelse(is.na(res), "empty", "main_menu"))
values$user_form_access <- allowed_schemas values$user_form_access <- allowed_schemas
scheme(values$user_form_access[1]) scheme(values$user_form_access[1])
mhcs(schms[[values$user_form_access[1]]]) mhcs(SCHMS[[values$user_form_access[1]]])
}) })
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -253,7 +241,6 @@ server <- function(input, output, session) {
hr(), hr(),
"Для начала работы нужно создать новую запись или загрузить существующую!", "Для начала работы нужно создать новую запись или загрузить существующую!",
hr(), hr(),
# сво
# загрузка панели для работы с базой данных # загрузка панели для работы с базой данных
uiOutput("admin_buttons_panel") uiOutput("admin_buttons_panel")
) )
@@ -280,6 +267,7 @@ server <- function(input, output, session) {
output$base_data <- renderUI({ output$base_data <- renderUI({
if (main_form_is_empty() == "main_menu") { if (main_form_is_empty() == "main_menu") {
con <- db$make_db_connection(scheme(),"base_data") con <- db$make_db_connection(scheme(),"base_data")
on.exit(db$close_db_connection(con, "base_data"), add = TRUE) on.exit(db$close_db_connection(con, "base_data"), add = TRUE)
@@ -292,13 +280,13 @@ server <- function(input, output, session) {
# задачи на сегодня # задачи на сегодня
if ("tasks" %in% DBI::dbListTables(con)) { 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() 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() 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() dplyr::pull()
} else { } else {
@@ -326,7 +314,7 @@ server <- function(input, output, session) {
observeEvent(input$schmes_selector, { observeEvent(input$schmes_selector, {
scheme(input$schmes_selector) scheme(input$schmes_selector)
mhcs(schms[[input$schmes_selector]]) mhcs(SCHMS[[input$schmes_selector]])
}) })
@@ -372,13 +360,13 @@ server <- function(input, output, session) {
} }
) )
exported_df <- setNames(exported_values, input_ids) |> exported_df <- stats::setNames(exported_values, input_ids) |>
as_tibble() dplyr::as_tibble()
# пайплайн для главной таблицы # пайплайн для главной таблицы
if (table_name == "main") { if (table_name == "main") {
exported_df <- exported_df |> exported_df <- exported_df |>
mutate( dplyr::mutate(
!!dplyr::sym(mhcs()$get_main_key_id) := values$main_key, !!dplyr::sym(mhcs()$get_main_key_id) := values$main_key,
.before = 1 .before = 1
) )
@@ -387,7 +375,7 @@ server <- function(input, output, session) {
# для всех остальных таблицы (вложенные) # для всех остальных таблицы (вложенные)
if (table_name != "main") { if (table_name != "main") {
exported_df <- exported_df |> exported_df <- exported_df |>
mutate( dplyr::mutate(
!!dplyr::sym(mhcs()$get_main_key_id) := values$main_key, !!dplyr::sym(mhcs()$get_main_key_id) := values$main_key,
!!dplyr::sym(nested_key_id) := values$nested_key, !!dplyr::sym(nested_key_id) := values$nested_key,
.before = 1 .before = 1
@@ -473,7 +461,7 @@ server <- function(input, output, session) {
# если ключ в формате даты - дать человекочитаемые данные # если ключ в формате даты - дать человекочитаемые данные
if (this_nested_form_key_scheme_smoll$form_type == "date") { if (this_nested_form_key_scheme_smoll$form_type == "date") {
kyes_for_this_table <- setNames( kyes_for_this_table <- stats::setNames(
kyes_for_this_table, kyes_for_this_table,
format(as.Date(kyes_for_this_table), "%d.%m.%Y") format(as.Date(kyes_for_this_table), "%d.%m.%Y")
) )
@@ -559,12 +547,12 @@ server <- function(input, output, session) {
str_cols <- which(col_types$form_type != "date") str_cols <- which(col_types$form_type != "date")
values$data <- values$data |> values$data <- values$data |>
select(-mhcs()$get_main_key_id) |> dplyr::select(-mhcs()$get_main_key_id) |>
mutate( dplyr::mutate(
dplyr::across(tidyselect::all_of({{date_cols}}), as.Date), dplyr::across(tidyselect::all_of({{date_cols}}), as.Date),
dplyr::across(tidyselect::all_of({{str_cols}}), as.character), dplyr::across(tidyselect::all_of({{str_cols}}), as.character),
) |> ) |>
arrange({{key_id}}) dplyr::arrange({{key_id}})
output$dt_nested <- DT::renderDataTable( output$dt_nested <- DT::renderDataTable(
DT::datatable( DT::datatable(
@@ -713,6 +701,7 @@ server <- function(input, output, session) {
mhcs = mhcs, mhcs = mhcs,
ns = NS(values$nested_form_id) ns = NS(values$nested_form_id)
) )
} else { } else {
utils$clean_forms(values$nested_form_id, mhcs(), NS(values$nested_form_id)) utils$clean_forms(values$nested_form_id, mhcs(), NS(values$nested_form_id))
} }
@@ -729,7 +718,7 @@ server <- function(input, output, session) {
ui1 <- rlang::exec( ui1 <- rlang::exec(
.fn = utils$render_forms, .fn = utils$render_forms,
!!!distinct(scheme_for_key_input, form_id, form_label, form_type), !!!dplyr::distinct(scheme_for_key_input, form_id, form_label, form_type),
main_scheme = scheme_for_key_input main_scheme = scheme_for_key_input
) )
@@ -781,7 +770,7 @@ server <- function(input, output, session) {
need(values$main_key, "⚠️ Необходимо указать id пациента!") need(values$main_key, "⚠️ Необходимо указать id пациента!")
) )
span( span(
strong("Таблица: "), names(enabled_schemes)[enabled_schemes == scheme()], strong("Таблица: "), names(ENABLED_SCHEMES)[ENABLED_SCHEMES == scheme()],
br(), br(),
strong("ID: "), values$main_key strong("ID: "), values$main_key
) )
@@ -813,7 +802,7 @@ server <- function(input, output, session) {
# создать форму для выбора ключа # создать форму для выбора ключа
ui1 <- rlang::exec( ui1 <- rlang::exec(
.fn = utils$render_forms, .fn = utils$render_forms,
!!!distinct(scheme_for_key_input, form_id, form_label, form_type), !!!dplyr::distinct(scheme_for_key_input, form_id, form_label, form_type),
main_scheme = scheme_for_key_input main_scheme = scheme_for_key_input
) )
@@ -910,7 +899,7 @@ server <- function(input, output, session) {
con <- db$make_db_connection(scheme(),"load_data_button") con <- db$make_db_connection(scheme(),"load_data_button")
on.exit(db$close_db_connection(con, "load_data_button")) on.exit(db$close_db_connection(con, "load_data_button"))
if (length(dbListTables(con)) != 0 && "main" %in% DBI::dbListTables(con)) { if (length(DBI::dbListTables(con)) != 0 && "main" %in% DBI::dbListTables(con)) {
# GET DATA files # GET DATA files
ids <- db$get_keys_from_table("main", mhcs(), con) ids <- db$get_keys_from_table("main", mhcs(), con)
@@ -922,7 +911,7 @@ server <- function(input, output, session) {
choices = ids, choices = ids,
selected = NULL, selected = NULL,
options = list( options = list(
placeholder = "id пациента", placeholder = "id",
onInitialize = I('function() { this.setValue(""); }') onInitialize = I('function() { this.setValue(""); }')
) )
) )
@@ -1020,7 +1009,8 @@ server <- function(input, output, session) {
date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE)
number_columns <- subset(scheme, form_type == "number", 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 |> df <- df |>
dplyr::mutate( dplyr::mutate(
@@ -1028,7 +1018,7 @@ server <- function(input, output, session) {
dplyr::across(tidyselect::all_of({{date_columns}}), as.Date), dplyr::across(tidyselect::all_of({{date_columns}}), as.Date),
# числа - к единому формату десятичных значений # числа - к единому формату десятичных значений
dplyr::across(tidyselect::all_of({{number_columns}}), ~ gsub("\\.", "," , .x)), 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)) dplyr::filter(!is.na(mhcs()$get_main_key_id))
@@ -1041,7 +1031,7 @@ server <- function(input, output, session) {
list_of_df[["meta"]] <- dplyr::tribble( list_of_df[["meta"]] <- dplyr::tribble(
~`Параметр` , ~`Значение`, ~`Параметр` , ~`Значение`,
"Пользователь" , values$current_user, "Пользователь" , values$current_user,
"Название базы" , names(enabled_schemes)[enabled_schemes == scheme()], "Название базы" , names(ENABLED_SCHEMES)[ENABLED_SCHEMES == scheme()],
"id базы" , scheme(), "id базы" , scheme(),
"id формы" , config::get("form_id"), "id формы" , config::get("form_id"),
"ver формы" , config::get("form_app_version"), "ver формы" , config::get("form_app_version"),
@@ -1238,7 +1228,8 @@ server <- function(input, output, session) {
date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) date_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE)
number_columns <- subset(scheme, form_type == "number", 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" # функция для преобразование числовых значений и сохранения "NA"
num_converter <- function(old_col) { num_converter <- function(old_col) {
@@ -1258,12 +1249,12 @@ 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({{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({{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)))) dplyr::select(tidyselect::all_of(unique(c(main_key_id, scheme$form_id))))
df_original <- DBI::dbReadTable(con, table_name) |> df_original <- DBI::dbReadTable(con, table_name) |>
as_tibble() dplyr::as_tibble()
if (input$upload_data_from_xlsx_owerwrite_all_data == TRUE) { if (input$upload_data_from_xlsx_owerwrite_all_data == TRUE) {
@@ -1272,15 +1263,24 @@ server <- function(input, output, session) {
} else { } else {
# удаление данных в базе данных по ключам # удаление данных в базе данных по ключам
walk( # purrr::walk(
.x = unique(df[[main_key_id]]), # .x = unique(df[[main_key_id]]),
.f = \(main_key) { # .f = \(main_key) {
if (main_key %in% unique(df_original[[main_key_id]])) { # if (main_key %in% unique(df_original[[main_key_id]])) {
DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE {main_key_id} = '{main_key}'")) # DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE {main_key_id} = '{main_key}'"))
} # }
} # }
) # )
# TODO
all_existed_keys <- unique(df_original[[main_key_id]])
all_new_keys <- unique(df[[main_key_id]])
keys_to_delete <- all_existed_keys[all_existed_keys %in% all_new_keys]
keys_to_delete <- paste0("'", keys_to_delete, "'", collapse = ", ")
DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE \"{main_key_id}\" IN ({keys_to_delete})"))
} }
@@ -1318,7 +1318,7 @@ server <- function(input, output, session) {
read_df_from_db_all <- function(table_name, con) { read_df_from_db_all <- function(table_name, con) {
# check if this table exist # check if this table exist
if (table_name %in% dbListTables(con)) { if (table_name %in% DBI::dbListTables(con)) {
# prepare query # prepare query
query <- glue::glue(" query <- glue::glue("
SELECT * FROM {table_name} SELECT * FROM {table_name}
@@ -1337,6 +1337,7 @@ server <- function(input, output, session) {
"loading data", "loading data",
"creating new key", "creating new key",
"exporting data to xlsx", "exporting data to xlsx",
"export validation dataset",
"importing data from xlsx" "importing data from xlsx"
), ),
key = NA, key = NA,
@@ -1345,7 +1346,7 @@ server <- function(input, output, session) {
action <- match.arg(action) action <- match.arg(action)
action_row <- tibble( action_row <- dplyr::tibble(
date = Sys.time(), date = Sys.time(),
user = values$current_user, user = values$current_user,
app_id = config::get("form_id"), app_id = config::get("form_id"),
@@ -1361,9 +1362,57 @@ server <- function(input, output, session) {
# TASKS --------------------------------------- # TASKS ---------------------------------------
tasks$server("tasks", values, scheme, mhcs) tasks$server("tasks", values, scheme, mhcs)
# 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)
app <- shinyApp(ui = ui, server = server) shiny::runApp(app, launch.browser = TRUE)
runApp(app, launch.browser = TRUE)

View File

@@ -1,7 +1,7 @@
default: default:
form_app_version: !expr config::get("form_app_version", file = "descr.yml") form_app_version: !expr config::get("form_app_version", file = "config/descr.yml")
form_id: !expr config::get("form_id", file = "descr.yml") form_id: !expr config::get("form_id", file = "config/descr.yml")
form_name: !expr config::get("form_name", file = "descr.yml") form_name: !expr config::get("form_name", file = "config/descr.yml")
prod: prod:
form_app_configure_path: "example_scheme" form_app_configure_path: "example_scheme"

4
config/descr.yml Normal file
View File

@@ -0,0 +1,4 @@
default:
form_app_version: 0.18.3
form_id: formy
form_name: FORMY

View File

@@ -1,4 +0,0 @@
default:
form_app_version: 0.17.0
form_id: new_formy
form_name: NEW FORMY

View File

@@ -1,117 +0,0 @@
options(box.path = here::here())
box::use(modules/data_manipulations[is_this_empty_value])
#' @export
init_val = function(scheme, ns) {
iv <- shinyvalidate::InputValidator$new()
# если передана функция с пространством имен, то происходит модификация id
if (!missing(ns)) {
scheme <- scheme |>
dplyr::mutate(form_id = ns(form_id))
}
# формируем список id - тип
inputs_simple_list <- scheme |>
dplyr::filter(!form_type %in% c("nested_forms", "description", "description_header")) |>
dplyr::distinct(form_id, form_type) |>
tibble::deframe()
# add rules to all inputs
purrr::walk(
.x = names(inputs_simple_list),
.f = \(x_input_id) {
form_type <- inputs_simple_list[[x_input_id]]
choices <- dplyr::filter(scheme, form_id == {{x_input_id}}) |>
dplyr::pull(choices)
val_required <- dplyr::filter(scheme, form_id == {{x_input_id}}) |>
dplyr::distinct(required) |>
dplyr::pull(required)
# 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") {
iv$add_rule(x_input_id, val_is_a_number)
# проверка на соответствие диапазону значений
if (!is.na(choices)) {
# разделить на несколько елементов
ranges <- as.integer(stringr::str_split_1(choices, "; "))
# проверка на кол-во значений
if (length(ranges) > 3) {
warning("Количество переданных элементов'", x_input_id, "' > 2")
} else {
iv$add_rule(x_input_id, val_number_within_a_range, ranges = ranges)
}
}
}
if (form_type %in% c("select_multiple", "select_one", "radio", "checkbox")) {
iv$add_rule(x_input_id, val_choice_within_a_dict, choices = choices)
}
# if in `required` column value is `1` apply standart validation
if (!is.na(val_required) && val_required == 1) {
iv$add_rule(x_input_id, shinyvalidate::sv_required(message = "Необходимо заполнить."))
}
}
)
iv
}
# работа с числовыми значениями ------------------
## проверка является ли значение числом ----------
val_is_a_number = function(x) {
# exit if empty
if (is_this_empty_value(x)) return(NULL)
# хак для пропуска значений
if (x == "NA") return(NULL)
# check for numeric
# if (grepl("^[-]?(\\d*\\,\\d+|\\d+\\,\\d*|\\d+)$", x)) NULL else "Значение должно быть числом."
if (grepl("^[+-]?\\d*[\\.|\\,]?\\d+$", x)) NULL else "Значение должно быть числом."
}
## находится ли число в заданном диапазоне значений -------
val_number_within_a_range = function(x, ranges) {
# exit if empty
if (is_this_empty_value(x)) return(NULL)
if (x == "NA") return(NULL)
# замена разделителя десятичных цифр
x <- stringr::str_replace(x, ",", ".")
# check for currect value
if (dplyr::between(as.double(x), ranges[1], ranges[2])) {
NULL
} else {
glue::glue("Значение должно быть между {ranges[1]} и {ranges[2]}.")
}
}
# списки ---------------------------------------------------------
## являются ли выбранные значения допустимы (согласно файлу схемы)
val_choice_within_a_dict = function(x, choices) {
if (length(x) == 1) {
if (is_this_empty_value(x)) return(NULL)
}
# проверка на соответствие вариантов схеме ---------
compare_to_dict <- (x %in% choices)
if (!all(compare_to_dict)) {
text <- paste0("'",x[!compare_to_dict],"'", collapse = ", ")
glue::glue("варианты, не соответствующие схеме: {text}")
}
}

View File

@@ -1,6 +1,6 @@
{ {
"R": { "R": {
"Version": "4.3.1", "Version": "4.3.2",
"Repositories": [ "Repositories": [
{ {
"Name": "CRAN", "Name": "CRAN",
@@ -717,6 +717,19 @@
], ],
"Hash": "b8552d117e1b808b09a832f589b79035" "Hash": "b8552d117e1b808b09a832f589b79035"
}, },
"lubridate": {
"Package": "lubridate",
"Version": "1.9.5",
"Source": "Repository",
"Repository": "CRAN",
"Requirements": [
"R",
"generics",
"methods",
"timechange"
],
"Hash": "07061b348d057e8ac86771e0eff36b62"
},
"magrittr": { "magrittr": {
"Package": "magrittr", "Package": "magrittr",
"Version": "2.0.3", "Version": "2.0.3",
@@ -1203,6 +1216,17 @@
], ],
"Hash": "79540e5fcd9e0435af547d885f184fd5" "Hash": "79540e5fcd9e0435af547d885f184fd5"
}, },
"timechange": {
"Package": "timechange",
"Version": "0.4.0",
"Source": "Repository",
"Repository": "CRAN",
"Requirements": [
"R",
"cpp11"
],
"Hash": "39c40cb1ad47a4cc384a34a22a29463f"
},
"tinytex": { "tinytex": {
"Package": "tinytex", "Package": "tinytex",
"Version": "0.46", "Version": "0.46",