knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
library(crudr)
This package is a (create-read-update-delete) CRUD app built on DT in R. It's a
Shiny interface where users directly edit cells within the presented datatable,
and the syncing between your user computer, the server, and the database happen
immediately and automatically in the background as you exit the cell. The idea
behind crudr
is to make it easy for you to centrally maintain a single source
of centralized data while at the same time maintaining a log of all changes and
allowing controlled but distributed editing of a dataset to keep it continuously
updated.
Companies often have different departments with different spreadsheets that track the same information. For instance, let's say you have a bunch of store locations in your company. Ideally, someone in your company has all the updated information about those locations, e.g. the store's mailing address, phone number, email, manager, manager's email, etc. In most companies, that information actually resides in a lot of different places and the datasets almost always have errors because managers move around, store leases expire, etc. Further, Sally might be in charge of the Locations, but Jose knows the Manager info, etc. You need that data to be correct, joined, posted, and available for everyone, at any time time, but maintaining quality data is hard to do. This package helps to solve the distributed-data-maintenance and distributed-data-availability problem.
The goals with crudr are to:
Present data from a relational database through a DataTable in a Shiny app for viewing and management
Separate data viewers from data managers / administrators
Make it easy to control which admin users can update columns in a DB table
Keep a record of all changes made to the database table in a corresponding change log table
Allow more than one simultaneous editor without crashes
Be database agnostic
Below is an example of a minimal app. This one is connected to a Postgres database for testing.
{width="100%"}
To install crudr, run:
remotes::install_github('eauleaf/crudr')
The Example is built from these packages, so you might want to install the below packages. Note: the package 'pool' must be the most updated version on CRAN.
install.packages(c( 'shiny', 'shinydashboard', 'tidyverse', 'lubridate', 'DT', 'pool', 'here' ))
There are really only 4 crudr functions you need to know:
cdr_make_db_tbls()
, cdr_manage_db_tbls()
,
cdr_deliver_admin_tbl()
, & cdr_deliver_chg_log_tbl()
.
This function, cdr_make_db_tbls()
, writes two tables into your database.
You hand this function a tibble and a pool connection, and it'll write that
table, as well as a corresponding change tracking table, into your database. If
you already have the main table in your database, no problem. Run the same
function with the same inputs and cdr_make_db_tbls()
will just create the
change tracking table. If you already have both tables, the function doesn't do
anything except write some notes out to your R console saying that it's not
going to mess with tables that already exist.
Note: For crudr to work, each row of the table you write must have a
Unique ID that's recorded as a character. If you don't have a Unique ID
key field in your table when , cdr_make_db_tbls()
initially writes the
table, then the function will automatically make one for you named
"UID".
If you want to create a database on your machine called 'test.db', and you want to put into your database a table of flower data called 'iris', you'd write:
con <- pool::dbPool(DBI::dbConnect(RSQLite::SQLite(), 'test.db'))
Then you'd call the function with the above connection and a table to
write. You can find this database by looking in your default directory with
here::here()
.
cdr_make_db_tbls(con, iris)
This code will join the new column, 'UID', onto your iris
data and
push the table to your test database.
If you already created a Unique ID key field in your table, just tell
the function the name of that field with
cdr_make_db_tbls(con, iris, key_field = 'name_of_your_unique_id_field')
.
Okay, the function should have written to your database a new table called 'iris' and a second table called 'iris_DELTAS'. The table names are case sensitive. You can see the tables that the function wrote by running the following:
paste(pool::dbListTables(con), collapse = ', ')
You should see the printout: "iris" and "iris_DELTAS". These two tables are your original table, but with a 'UID' field, and a secondary table named similar to your first, but with '*_DELTAS'. The 'iris_DELTAS' deltas table is a change log. Any change you make to the primary table through the admin user interface, which we'll create below, gets recorded in the deltas table.
As a side note, if you want to remove the tables later, you can do that with:
pool::dbRemoveTable(con,'iris') pool::dbRemoveTable(con,'iris_DELTAS')
Or, just delete the database 'test.db'.
This function, cdr_manage_db_tbls()
, is the workhorse of the package. If you remember one
function, this is the one. The function has only a handful of arguments.
At a minimum, you have to tell this function:
That's about it. There are other arguments you can change if you want to control who can update things in the administrator table, but you can just read the function details later for that info.
So, cdr_manage_db_tbls()
returns 1 table directly and 2 tables
invisibly.
The direct output table in the example app below goes into: iris_r_tbl
, as in
iris_r_tbl <- cdr_manage_db_tbls()
. Because the output is a reactive table,
you use the variable by acting like it's a function, i.e. by putting
parentheses, '()', after the variable name. So, if you wanted to select some
columns or filter some rows, you'd write, iris_r_tbl() %>% select(some_columns)
%>% filter(some_rows)
, with the parentheses after 'iris_r_tbl', but otherwise
it works just like with any other dataframe object.
The output table from cdr_manage_db_tbls()
, in this case named,
iris_r_tbl()
is yours; you can do whatever you want with it. You can
send it out in any format you like, change the column names, filter it,
join it to something else, send it into a 'leaflet' map...whatever. It's
an output to present to the masses. In fact, you don't even have to
capture this output from cdr_manage_db_tbls()
if you don't want to.
However, the 2 tables that cdr_manage_db_tbls()
returns invisibly you
have to pick up with the functions cdr_deliver_admin_tbl()
&
cdr_deliver_chg_log_tbl()
. The tables these functions return are not
like the direct output table. Rather, these tables are direct
representations of what's in your database. Let's look at these 2
functions.
This function picks up your invisible administrator table already
rendered in a DT object. The table is the primary database table that
you wrote into your test database when you ran cdr_make_db_tbls()
, so
in our test case, cdr_deliver_admin_tbl('iris')
will deliver 'iris'
data for a data curator. That is, whoever maintains your primary dataset
should have access to this admin table, and that person can make changes
to the data held in the database via this output in Shiny. You call
cdr_deliver_admin_tbl('iris')
in the UI portion of Shiny.
A full example is below in the section named CRUDR Example Code.
This function picks up your invisible history-of-changes table already
rendered in a DT object. This table is the '*_DELTAS' database table
that you wrote into your test database when you ran
cdr_make_db_tbls()
, so in our test case,
cdr_deliver_chg_log_tbl('iris')
will deliver a change history about
changes that your data curators made to the table 'iris'. That is, crudr
records every change that someone makes to your primary dataset within
this change-log table. You don't need to do much with this table. It's
just there to record which administrator did what to the database table,
and when they made the change. You also call this function in the UI
portion of Shiny. A full example is below in the section named CRUDR
Example Code. This table should be empty right now, but let's change
that.
To manage the database tables, let's launch a bare-bones shiny app. We'll only
need the single 'crudr' functions described above, cdr_manage_db_tbls
. The
other code all comes from packages shiny, shinydashboard, and DT. Copy the
code below into your RStudio console, run it, and then play with your data.
con <- pool::dbPool(DBI::dbConnect(RSQLite::SQLite(), 'test.db')) crudr::cdr_make_db_tbls(con, iris) header <- shinydashboard::dashboardHeader(title = 'Tiny CRUDR Example') sidebar <- shinydashboard::dashboardSidebar( shinydashboard::sidebarMenu( id = 'tabs', shinydashboard::menuItem( text = "Iris Data", startExpanded = TRUE, shinydashboard::menuSubItem("Administrator Table", tabName = "datr_editable", icon = shiny::icon('edit')), shinydashboard::menuSubItem("Change Log", tabName = "datr_change_log"), shinydashboard::menuSubItem("Iris End User View", tabName = "datr_end_usr") ))) body <- shinydashboard::dashboardBody( shinydashboard::tabItems( shinydashboard::tabItem(tabName = "datr_editable", crudr::cdr_deliver_admin_tbl('iris')), shinydashboard::tabItem(tabName = "datr_change_log", crudr::cdr_deliver_chg_log_tbl('iris')), shinydashboard::tabItem(tabName = "datr_end_usr", DT::DTOutput('iris_db_data')) )) ui <- shinydashboard::dashboardPage(header, sidebar, body) server <- function(input, output, session){ iris_r_tbl <- crudr::cdr_manage_db_tbls( db_tbl_name = 'iris', key_col = 'UID', conn_pool = con, session = session, add_row_permission = T, del_row_permission = T, cell_edit_permission = T, lock_fields = c() ) output$iris_db_data <- DT::renderDT( DT::formatDate(table = DT::datatable(iris_r_tbl()), columns = c('WHEN_EDITED_LAST'), method = 'toLocaleString') ) } shiny::shinyApp(ui, server)
In the app above, you're the admin. So, go ahead and make some changes
in this administrator table. Double-click on a cell or press 'F2' to
enter edit mode, and then change the data. Your app will write your
change to the database, update the change log table, write the
change-log data to the database, and then await your next change. The
'Iris End User View' corresponds to the iris_r_tbl()
direct output
from the server. Make your direct output awesome, like add some buttons
to it by dropping in some DT code:
DT::datatable(iris_r_tbl(), extensions = 'Buttons', options = list(dom = 'tB', buttons = c('copy', 'csv', 'excel','pdf')))
That's pretty much it. Close down the app. Then reopen it by calling the same app code again. The app should load the data from the database and all the changes that you made to the data should be there.
If you want to see how crudr acts with multiple admin users, open a second app and then go back and forth making some data changes in each app. When you make a change, crudr checks if anyone else made a change and refreshes if so.
If you want to set admin permissions for specific people, you can do so in your shiny server with some code like this:
user <- ifelse(is.null(session$user), Sys.info()[['user']], session$user) if (user == 'your.name'){ okay_to_change = T } else { okay_to_change = F }
Then pass your 'okay_to_change' variable into the cdr_manage_db_tbls()
permission argument(s) like 'cell_edit_permission = okay_to_change' or
'add_row_permission = okay_to_change'. A FALSE in these parameters makes
the admin table just another table when a typical user opens it, but
whoever signs is as 'your.name' can change the data in the database that
everyone else sees. Alternatively, you could just write your code to
not present to certain users the tables from cdr_deliver_admin_tbl()
or cdr_deliver_chg_log_tbl()
at all in the UI, it's up to you.
For a Postgres database connection, you can only use a pool
connection with RPostgreSQL::PostgreSQL()
, but RPostgres::Postgres()
also works fine--just don't wrap it in pool::dbPool()
.
Example Postgres connection strings below.
RPostgreSQL::PostgreSQL():
con = pool::dbPool( DBI::dbConnect( drv = RPostgreSQL::PostgreSQL(), dbname = "test", host = "localhost", port = "5432", user = your_dsn_uid, password = your_dsn_pwd ) )
RPostgres::Postgres():
con <- DBI::dbConnect( drv = RPostgres::Postgres(), dbname = "test", host = "localhost", user = your_dsn_uid, password = your_dsn_pwd )
To close your database connection, run: pool::poolClose(con)
or
DBI::dbDisconnect(con)
There are quite a few things I should to do to make crudr better. Maybe I'll get to these sometime.
Support for more databases, like sqlserver, oracle, etc.
When 2 or more admin users are making concurrent changes, update just the specific elements that the other person changed rather than refreshing the whole table.
Create a function "cdr_reconstruct_past_tbl(as_of = 'a prior Sys.time()')", where you hand a datetime to the function and the function returns a tibble of the database admin table as it was as of that particular point in time.
Figure out how to push data to the database asynchronously.
Write a bunch of tests for the functions.
Maybe add row lockout controls.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.