Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finally implement row review #146

Open
wants to merge 87 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
2d0d704
Add very basic checkbox column to tables
jthompson-arcus Nov 21, 2024
d436332
Merge branch 'jt-99-review_by_row' into jt-99-review_by_row_for_reals
jthompson-arcus Nov 21, 2024
f56c780
Merge branch 'jt-99-review_by_row' into jt-99-review_by_row_for_reals
jthompson-arcus Nov 22, 2024
67d817f
"Highlight" rows when check status changed
jthompson-arcus Nov 22, 2024
7939b3c
Grab id's for `o_reviewed`
jthompson-arcus Nov 22, 2024
e3bb3ed
Add input handler
jthompson-arcus Nov 22, 2024
6e258a2
Capture row index info
jthompson-arcus Nov 25, 2024
a42049b
Standardize callbacks and renders
jthompson-arcus Nov 25, 2024
7ba17d9
Add missing observers
jthompson-arcus Nov 25, 2024
f0dfcc3
Add study form observers
jthompson-arcus Nov 25, 2024
9f0e4de
Add `anti_join()`s back
jthompson-arcus Nov 25, 2024
b703bec
First pass at integration in review module
jthompson-arcus Nov 25, 2024
5fd5747
Fix couple of issues
jthompson-arcus Nov 25, 2024
5956269
Remove row selection
jthompson-arcus Nov 25, 2024
d9f49e8
Fix bug on study forms
jthompson-arcus Nov 25, 2024
e2b9953
Resolve merge conflict with `jt-99-review_by_row`
jthompson-arcus Dec 2, 2024
2493779
Integrate "form_reviewed" checkbox
jthompson-arcus Dec 2, 2024
0cb8710
Update tables from overall checkbox
jthompson-arcus Dec 2, 2024
2894351
Update `app-feature-1` snapshots
jthompson-arcus Dec 2, 2024
b27d95d
Update app-feature-3-002.json
jthompson-arcus Dec 2, 2024
36f843a
Update app_feature_04.md
jthompson-arcus Dec 2, 2024
e98afa0
Create custom.js
jthompson-arcus Dec 2, 2024
a353f5d
Fix warnings and errors in `mod_common_forms` test
jthompson-arcus Dec 2, 2024
579d80e
Repair part of `mod_review_form` tests
jthompson-arcus Dec 3, 2024
a7fda21
Properly handle partially reviewed rows
jthompson-arcus Dec 3, 2024
310eff2
Repair interactivity between overall checkbox and tables
jthompson-arcus Dec 3, 2024
5a78643
Clean up table name
jthompson-arcus Dec 3, 2024
365206d
Update `app_feature_01` JSONs
jthompson-arcus Dec 3, 2024
d10e475
Fix issue with setting input for checkbox
jthompson-arcus Dec 3, 2024
1982650
Update `app_feature_03` JSONs
jthompson-arcus Dec 3, 2024
5d45431
Reset review reactiveValues after save
jthompson-arcus Dec 3, 2024
96e795e
Repair study form test
jthompson-arcus Dec 3, 2024
f4a71ad
Update test-mod_review_forms.R
jthompson-arcus Dec 3, 2024
54f7f55
Add rudimentary progress bar
jthompson-arcus Dec 4, 2024
2473133
Add transition to progress bar
jthompson-arcus Dec 4, 2024
3123136
Wrap Shiny bindings
jthompson-arcus Dec 5, 2024
b987708
Clean up readability of checkbox render function
jthompson-arcus Dec 5, 2024
f637dd0
Update `app-feature-01` JSONs
jthompson-arcus Dec 5, 2024
7144bd8
Update `app-feature-02` JSON
jthompson-arcus Dec 5, 2024
e6fe7ee
Update `app-feature-03` JSONs
jthompson-arcus Dec 5, 2024
d6a2ff9
Add bottom margin to progress bar
jthompson-arcus Dec 5, 2024
f0a19e7
Update `mod_study_forms` JSONs
jthompson-arcus Dec 5, 2024
bfc864f
Spruce up progress bar output object
jthompson-arcus Dec 5, 2024
e9ab76d
Update custom.css
jthompson-arcus Dec 5, 2024
c6ef036
Fix "form already reviewed" indicator
jthompson-arcus Dec 5, 2024
a36d7cf
Update version
jthompson-arcus Dec 5, 2024
a746b2e
Only review selected subject
jthompson-arcus Dec 5, 2024
546b4e4
Save updated status in DOM
jthompson-arcus Dec 5, 2024
c07540a
Set `server=FALSE` for the moment
jthompson-arcus Dec 5, 2024
39238a3
Switch back to using server for datatables
jthompson-arcus Dec 6, 2024
70ad863
Improve `update_cbs()`
jthompson-arcus Dec 6, 2024
5c64e08
Resolve issue for partially reviewed rows
jthompson-arcus Dec 9, 2024
02e65df
Return row IDs as well
jthompson-arcus Dec 10, 2024
a82bec9
Use `colnames` instead of renaming data frame
jthompson-arcus Dec 10, 2024
f3f8247
Remove `plugin = "scrollResize"` from custom datatable
jthompson-arcus Dec 13, 2024
0bb7ee6
Larger rewrite to use datatable proxy objects
jthompson-arcus Dec 13, 2024
05084db
Simplify DT callbacks
jthompson-arcus Dec 13, 2024
ebfd724
Update app feature JSONs
jthompson-arcus Dec 13, 2024
33b94f4
Resolve some issues with app feature 4
jthompson-arcus Dec 13, 2024
36a7443
Separate data reload from update
jthompson-arcus Dec 17, 2024
fa06ef3
Fix bug with progress bar when datatable is not rendered
jthompson-arcus Dec 17, 2024
689e525
Show 100% completed when no data to review
jthompson-arcus Dec 17, 2024
cc34876
Update test-datatable_custom.R
jthompson-arcus Dec 17, 2024
3cde826
Resolve merge conflict with `dev`
jthompson-arcus Dec 17, 2024
83642d8
Fix bug on initialization
jthompson-arcus Dec 30, 2024
ee4fdad
Separate `o_reviewed` logic into helper function
jthompson-arcus Dec 31, 2024
fa9ded8
Create helper function for review selection UPSERT
jthompson-arcus Dec 31, 2024
aa05fbb
Document `o_reviewed` field
jthompson-arcus Dec 31, 2024
5176d1f
Resolve documentation error
jthompson-arcus Dec 31, 2024
2feb93a
Add helper function to update reactive table from user selection
jthompson-arcus Dec 31, 2024
103d282
Move logic to module
jthompson-arcus Dec 31, 2024
fa06a00
Add table title back
jthompson-arcus Dec 31, 2024
f5fa833
Update documentation
jthompson-arcus Dec 31, 2024
8a200f4
Delete update_tbl_data_from_datatable.Rd
jthompson-arcus Dec 31, 2024
dec6f34
Update app feature 01 test JSONs and PNGs
jthompson-arcus Dec 31, 2024
db1b0ce
Update app-feature-3-002.json
jthompson-arcus Dec 31, 2024
53eac61
Update app_feature_04.md
jthompson-arcus Dec 31, 2024
dbdfc20
Initialize `disabled` element on data creation
jthompson-arcus Dec 31, 2024
7c61998
Update app_feature_04.md
jthompson-arcus Dec 31, 2024
b1b514a
Update test-mod_common_forms.R
jthompson-arcus Dec 31, 2024
9481c0e
Update `mod_navigate_review` JSONs
jthompson-arcus Dec 31, 2024
0a96d60
Repair part of study forms tests
jthompson-arcus Dec 31, 2024
add5001
Add back table check for common forms
jthompson-arcus Dec 31, 2024
1e94521
Finish repairing study forms tests
jthompson-arcus Dec 31, 2024
10fe949
Update mod_report-001.json
jthompson-arcus Dec 31, 2024
6e03b44
Update `mod_review_form_tbl_server()` documentation
jthompson-arcus Dec 31, 2024
3d14c53
Update global.R
jthompson-arcus Dec 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: clinsight
Title: ClinSight
Version: 0.1.1.9012
Version: 0.1.1.9013
Authors@R: c(
person("Leonard Daniël", "Samson", , "lsamson@gcp-service.com", role = c("cre", "aut"),
comment = c(ORCID = "0000-0002-6252-7639")),
Expand Down
3 changes: 3 additions & 0 deletions R/app_server.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ app_server <- function(
})
check_appdata(app_data, meta)

session$userData$review_records <- reactiveValues()
session$userData$update_checkboxes <- reactiveValues()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand it correctly, the variable 'update_checkboxes' is only TRUE, FALSE, or NULL, based on the value of input$form_reviewed . Why is it called update_checkboxes and not form_reviewed or something similar, for consistency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming conventions are a bit in the eye of the beholder, but I named it update_checkboxes because that is the action the reactive is meant to trigger. Plus it would be inaccurate to call it form_reviewed because the value is more tied to changes in the sidebar checkbox, not to the review status of a form.


res_auth <- authenticate_server(
all_sites = app_vars$Sites$site_code,
credentials_db = credentials_db,
Expand Down
15 changes: 9 additions & 6 deletions R/fct_data_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,6 @@ add_missing_columns <- function(
#' @param title Optional. Character string with the title of the table.
#' @param selection See [DT::datatable()]. Default set to 'single'.
#' @param extensions See [DT::datatable()]. Default set to 'Scroller'.
#' @param plugins See [DT::datatable()]. Default set to 'scrollResize'.
#' @param dom See \url{https://datatables.net/reference/option/dom}. A div
#' element will be inserted before the table for the table title. Default set
#' to 'fti' resulting in 'f<"header h5">ti'.
Expand Down Expand Up @@ -544,15 +543,16 @@ datatable_custom <- function(
title = NULL,
selection = "single",
extensions = c("Scroller", "ColReorder"),
plugins = "scrollResize",
dom = "fti",
options = list(),
...
){
stopifnot(is.data.frame(data))
colnames <- names(data)
if(!is.null(rename_vars)){
stopifnot(is.character(rename_vars))
data <- dplyr::rename(data, dplyr::any_of(rename_vars))
colnames <- dplyr::rename(data[0,], dplyr::any_of(rename_vars)) |>
names()
}
stopifnot(is.null(title) | is.character(title))
stopifnot(grepl("t", dom, fixed = TRUE))
Expand All @@ -563,9 +563,12 @@ datatable_custom <- function(
scrollX = TRUE,
scroller = TRUE,
deferRender = TRUE,
scrollResize = TRUE,
scrollCollapse = TRUE,
colReorder = TRUE
colReorder = list(
enable = TRUE,
realtime = FALSE,
fixedColumnsLeft = 1
)
)
fixed_opts <- list(
initComplete = DT::JS(
Expand All @@ -588,7 +591,7 @@ datatable_custom <- function(
selection = selection,
options = opts,
extensions = extensions,
plugins = plugins,
colnames = colnames,
...
)
}
88 changes: 88 additions & 0 deletions R/fct_form_helpers.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#' Update Review Records
#'
#' Updates the review records data frame when a datatable checkbox is clicked.
#'
#' @param review_records The review records data frame to update.
#' @param review_selection The review selection data frame input from the
#' datatable.
#' @param active_data The active review data frame.
#'
#' @return A data frame containing the updated records data.
#'
#' @details Three main steps are performed: UPSERT, SUBSET, and ANTI-JOIN The
#' UPSERT takes the review selection data frame and upserts it into the review
#' records data frame. (An upsert will insert a record if the unique
#' identifier is not yet present and update a record based on the unique
#' identifier if it already exists.) The SUBSET step removes an empty reviews
#' (partially review rows) and any records not part of the active review (as a
#' precautionary measure). The ANTI-JOIN step removes any records that match
#' the active review (records that will not be changing review status based on
#' user inputs).
#'
#' @noRd
update_review_records <- function(review_records, review_selection, active_data) {
if (is.null(review_records))
review_records <- data.frame(id = integer(), reviewed = character())
review_records |>
dplyr::rows_upsert(
review_selection,
by = "id"
) |>

Check warning on line 30 in R/fct_form_helpers.R

View check run for this annotation

Codecov / codecov/patch

R/fct_form_helpers.R#L24-L30

Added lines #L24 - L30 were not covered by tests
# Remove empty reviews and inactive data IDs
subset(!is.na(reviewed) | !id %in% active_data$id) |>

Check warning on line 32 in R/fct_form_helpers.R

View check run for this annotation

Codecov / codecov/patch

R/fct_form_helpers.R#L32

Added line #L32 was not covered by tests
# Only update records where the review status is being changed
dplyr::anti_join(
active_data,
by = c("id", "reviewed")
) |>
dplyr::arrange(id)

Check warning on line 38 in R/fct_form_helpers.R

View check run for this annotation

Codecov / codecov/patch

R/fct_form_helpers.R#L34-L38

Added lines #L34 - L38 were not covered by tests
}

#' Update Server Table from Selection
#'
#' Updates the server table object based on the user selection.
#'
#' @param tbl_data A data frame containing the server table.
#' @param review_selection The review selection data frame input from the
#' datatable.
#'
#' @return A data frame containing the updated table data.
#'
#' @noRd
update_tbl_data_from_datatable <- function(tbl_data, review_selection) {
update_row <- dplyr::distinct(review_selection, reviewed, row_id)
row_ids <- tbl_data$o_reviewed |> lapply(\(x) x[["row_id"]]) |> unlist()
tbl_data[row_ids == update_row$row_id, "o_reviewed"] <- list(list(
modifyList(tbl_data[row_ids == update_row$row_id,]$o_reviewed[[1]],
list(updated = switch(update_row$reviewed, "Yes" = TRUE, "No" = FALSE, NA)))
))
tbl_data

Check warning on line 59 in R/fct_form_helpers.R

View check run for this annotation

Codecov / codecov/patch

R/fct_form_helpers.R#L53-L59

Added lines #L53 - L59 were not covered by tests
}

#' Overall Reviewed Field
#'
#' This field serves as the main communication mechanism between the Shiny
#' session and the DataTable objects in the browser.
#'
#' @format A list with up to five elements:
#' \describe{
#' \item{reviewed}{A logical indicating the current review status of the table row.}
#' \item{ids}{A vectors containing the `id`s associated with the table row.}
#' \item{row_id}{A numeric value indicating the associated row in the DataTable. (Used to update server data set based on user changes to browser table.)}
#' \item{disabled}{A logical indicating whether the table row is part of the active review.}
#' \item{updated}{A logical indicating whether the user has changed the review status in the DataTable.}
#' }
#'
#' @details The first three elements, `reviewed`, `ids`, and `row_id`, are
#' initialized when the datatable data set is created (via `create_table()`
#' etc.). This occurs whenever there is a change with the review data. The
#' `disabled` element gets updated whenever there is a change in which subject
#' is actively being reviewed. The `updated` field gets changed in one of
#' three events: the subject being reviewed is changed and `updated` gets set
#' to `NULL`, a user changed review status in the DataTable object and
#' `updated` gets set to the user inputted value, and finally when a user
#' changes the overall review status in the sidebar and `updated` gets set to
#' reflect that inputted value.
#'
#' @noRd
# "o_reviewed"
37 changes: 35 additions & 2 deletions R/fct_tables.R
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,51 @@ create_table.default <- function(
stopifnot(is.character(keep_vars))
stopifnot(is.character(name_column))
stopifnot(is.character(value_column))
if ("reviewed" %in% names(data)) {
data <- add_o_reviewed(data, keep_vars)
keep_vars <- c("o_reviewed", keep_vars)
}
df <- data[c(keep_vars, name_column, value_column)] |>
tidyr::pivot_wider(
names_from = {{name_column}},
values_from = {{value_column}},
values_fn = ~paste0(., collapse = "; ")
)
)
expected_columns <- na.omit(expected_columns) %||% character(0)
if(length(expected_columns) == 0) return(df)
add_missing_columns(df, expected_columns)[
unique(c(keep_vars, expected_columns))
]
}

#' Add Overall Reviewed Field
#'
#' Adds a field to the data set summarizing the overall review status over the
#' rows uniquely defined by the ID columns.
#'
#' @param data A data frame to mutate
#' @param id_cols A set of columns that uniquely identify each observation
#'
#' @details This function servers as a helper to `create_table.default()`. If
#' the field `reviewed` is contained in the data frame, an overall review status
#' field will be added to the data frame. The field is a list consistent of two
#' named elements: `reviewed` and `ids`. The `reviewed` field is `TRUE` if all
#' records are reviewed, `FALSE` if all records are not reviewed, and `NA` if
#' some records are reviewed and some are not. The `ids` field contains a vector
#' of the IDs associated with the unique observation defined by `id_cols`.
#'
#' @noRd
add_o_reviewed <- function(data, id_cols) {
dplyr::mutate(
data,
o_reviewed = dplyr::case_when(
any(reviewed == "No") & any(reviewed == "Yes") ~ list(list(reviewed = NA, ids = id)),
any(reviewed == "Yes") ~ list(list(reviewed = TRUE, ids = id)),
.default = list(list(reviewed = FALSE, ids = id))
),
.by = dplyr::all_of(id_cols))
}


#' Create Table with continuous data.
#'
Expand Down Expand Up @@ -227,7 +259,7 @@ create_table.adverse_events <- function(
keep_vars, expected_columns) |>
adjust_colnames("^AE ")
df[["Number"]] <- NULL

# create new row when an AE gets worse:
df_worsening <- df[!is.na(df[[worsening_start_column]]), ] |>
dplyr::mutate(
Expand Down Expand Up @@ -298,6 +330,7 @@ create_table.medication <- function(
) |>
dplyr::arrange(dplyr::desc(in_use), dplyr::desc(`Start Date`)) |>
dplyr::select(
dplyr::any_of("o_reviewed"),
dplyr::all_of(c(keep_vars, "Name")),
dplyr::everything(),
-dplyr::all_of(c("in_use", "Active Ingredient", "Trade Name",
Expand Down
5 changes: 4 additions & 1 deletion R/global.R
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ utils::globalVariables(
"event_id",
"region",
"suffix_names",
"form_type"
"form_type",
"id",
"o_reviewed",
"row_id"
)
)

Expand Down
109 changes: 56 additions & 53 deletions R/mod_common_forms.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ mod_common_forms_ui <- function(id, form){
bslib::layout_sidebar(
fillable = FALSE,
if(form == "Adverse events"){
DT::dataTableOutput(ns("SAE_table"))
mod_review_form_tbl_ui(ns("review_form_SAE_tbl"))
},
DT::dataTableOutput(ns("common_form_table")),
mod_review_form_tbl_ui(ns("review_form_tbl")),
sidebar = bslib::sidebar(
bg = "white",
position = "right",
Expand Down Expand Up @@ -73,7 +73,8 @@ mod_common_forms_ui <- function(id, form){
#' interactive tables.
#'
#' @seealso [mod_common_forms_ui()], [mod_timeline_ui()],
#' [mod_timeline_server()]
#' [mod_timeline_server()], [mod_review_form_tbl_ui()],
#' [mod_review_form_tbl_server()]
#'
mod_common_forms_server <- function(
id,
Expand All @@ -93,62 +94,64 @@ mod_common_forms_server <- function(
moduleServer( id, function(input, output, session){
ns <- session$ns

data_active <- reactive({
shiny::validate(need(
!is.null(r$filtered_data[[form]]),
paste0("Warning: no data found in the database for the form '", form, "'.")
))
df <- dplyr::left_join(
r$filtered_data[[form]],
with(r$review_data, r$review_data[item_group == form, ]) |>
dplyr::select(-dplyr::all_of(c("edit_date_time", "event_date"))),
by = id_item
) |>
dplyr::mutate(
item_value = ifelse(
reviewed == "No",
paste0("<b>", htmltools::htmlEscape(item_value), "*</b>"),
htmltools::htmlEscape(item_value)
)
common_form_data <- reactiveVal()
SAE_data <- reactiveVal()
observe({
df <- {
shiny::validate(need(
!is.null(r$filtered_data[[form]]),
paste0("Warning: no data found in the database for the form '", form, "'.")
))
dplyr::left_join(
r$filtered_data[[form]],
with(r$review_data, r$review_data[item_group == form, ]) |>
dplyr::select(-dplyr::all_of(c("edit_date_time", "event_date"))),
by = id_item
) |>
create_table(expected_columns = names(form_items))
if(!input$show_all_data){
df <- with(df, df[subject_id == r$subject_id, ])
dplyr::mutate(
item_value = ifelse(
reviewed == "No",
paste0("<b>", htmltools::htmlEscape(item_value), "*</b>"),
htmltools::htmlEscape(item_value)
)
) |>
create_table(expected_columns = names(form_items)) |>
dplyr::mutate(o_reviewed = Map(\(x, y, z) append(x, list(row_id = y, disabled = z)),
o_reviewed,
dplyr::row_number(),
subject_id != r$subject_id))
}
df
common_form_data({
if(form == "Adverse events") {
df |>
dplyr::filter(!grepl("Yes", `Serious Adverse Event`)
) |>
dplyr::select(-dplyr::starts_with("SAE"))
} else {
df
}
})
if (form == "Adverse events")
SAE_data({
df |>
dplyr::filter(grepl("Yes", `Serious Adverse Event`)) |>
dplyr::select(dplyr::any_of(
c("o_reviewed", "subject_id","form_repeat", "Name", "AESI", "SAE Start date",
"SAE End date", "CTCAE severity", "Treatment related",
"Treatment action", "Other action", "SAE Category",
"SAE Awareness date", "SAE Date of death", "SAE Death reason")
)) |>
adjust_colnames("^SAE ")
})
})
observeEvent(common_form_data(), {
mod_review_form_tbl_server("review_form_tbl", r, common_form_data, form, reactive(input$show_all_data), table_names, form)
if (form == "Adverse events")
mod_review_form_tbl_server("review_form_SAE_tbl", r, SAE_data, form, reactive(input$show_all_data), table_names, "Serious Adverse Events")
}, once = TRUE)

mod_timeline_server("timeline_fig", r = r, form = form)

output[["SAE_table"]] <- DT::renderDT({
req(form == "Adverse events")
SAE_data <- data_active() |>
dplyr::filter(grepl("Yes", `Serious Adverse Event`)) |>
dplyr::select(dplyr::any_of(
c("subject_id","form_repeat", "Name", "AESI", "SAE Start date",
"SAE End date", "CTCAE severity", "Treatment related",
"Treatment action", "Other action", "SAE Category",
"SAE Awareness date", "SAE Date of death", "SAE Death reason")
)) |>
adjust_colnames("^SAE ")
if(!input$show_all_data) SAE_data$subject_id <- NULL
datatable_custom(SAE_data, rename_vars = table_names, rownames= FALSE,
title = "Serious Adverse Events", escape = FALSE)
})

output[["common_form_table"]] <- DT::renderDT({
df <- data_active()
if(form == "Adverse events") {
df <- df |>
dplyr::filter(!grepl("Yes", `Serious Adverse Event`)
) |>
dplyr::select(-dplyr::starts_with("SAE"))
}
if(!input$show_all_data) df$subject_id <- NULL
datatable_custom(df, rename_vars = table_names, rownames= FALSE,
title = form, escape = FALSE)
})

})
}

Expand Down
Loading
Loading