shiny.reglog package requires user to create dbConnector and mailConnector for the RegLogServer functionality. While creating ShinyApp almost always you want to use one database and one emailing procedure. For all usage beyond RegLogServer defaults, it would be suboptimal to define new connections. That’s why during development I came to the conclusion that these connectors should allow for easy extensions with custom functions.
The scope of this vignette is to describe:
Both dbConnectors (RegLogDBIConnector
and
RegLogGsheetConnector
) and mailConnectors
(RegLogEmayiliConnector
and
RegLogGmailrConnector
) inherits from more general class:
RegLogConnector
. There are three public fields that are key
for the whole dataflow:
RegLogConnector$listener()
- reactiveVal
object that intakes RegLogConnectorMessage object.
RegLogConnector listens to any change in the value of this this
object and reacts accordingly.RegLogConnector$handlers
- named list of
handler functions. For every type
of
received message there should be a specific function appended there. All
of them should return another RegLogConnectorMessage object
informing about the result of the function.RegLogConnector$message()
- reactiveVal object
containing RegLogConnectorMessage that are returned from
handler function.RegLogConnector
object reacts upon receiving some kind
of RegLogConnectorMessage object and responds likewise. Its an
S3
class object that contains four fields:
time
: timestamp on which the message was generatedtype
: character string declaring handler function of
the RegLogConnector that should be called when object receive
message.data
: list of objects, usually some kind of input or
output of the handler functionlogcontent
: character string declaring content that
should be saved into RegLogConnector logs.You can create message freely using function of the same name:
<-
message RegLogConnectorMessage(
type = "test",
dataframe = mtcars,
numbers = runif(10, 0, 100),
logcontent = paste0("I contain data.frame and random numbers"))
str(message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.050"
#> $ type : chr "test"
#> $ data :List of 2
#> ..$ dataframe:'data.frame': 32 obs. of 11 variables:
#> .. ..$ mpg : num [1:32] 21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
#> .. ..$ cyl : num [1:32] 6 6 4 6 8 6 8 4 4 6 ...
#> .. ..$ disp: num [1:32] 160 160 108 258 360 ...
#> .. ..$ hp : num [1:32] 110 110 93 110 175 105 245 62 95 123 ...
#> .. ..$ drat: num [1:32] 3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
#> .. ..$ wt : num [1:32] 2.62 2.88 2.32 3.21 3.44 ...
#> .. ..$ qsec: num [1:32] 16.5 17 18.6 19.4 17 ...
#> .. ..$ vs : num [1:32] 0 0 1 1 0 1 0 1 1 1 ...
#> .. ..$ am : num [1:32] 1 1 1 0 0 0 0 0 0 0 ...
#> .. ..$ gear: num [1:32] 4 4 4 3 3 3 3 4 4 4 ...
#> .. ..$ carb: num [1:32] 4 4 1 1 2 1 4 2 2 4 ...
#> ..$ numbers : num [1:10] 12.6 24.2 77.2 70.2 99.3 ...
#> $ logcontent: chr "I contain data.frame and random numbers"
#> - attr(*, "class")= chr "RegLogConnectorMessage"
Both RegLogDBIConnector and RegLogGsheetConnector
contain the same default handler functions. In this vignette I will
focus on the messages that are received by the handler functions and
their general usability. To learn about messages produced by these
functions, check “RegLogServer object fields and methods” vignette and
its “Message” section - as all of these messages are finally exposed in
RegLogServer$message()
public field.
All of these functions aren’t exported, as they are used only
internally. You can read the documentation for them though with usual
syntax of ?function
in console. Documentation is rendered
for information how to react with them by creating
RegLogConnectorMessages yourself.
DBI_login_handler
gsheet_login_handler
These functions are handling querying the database for the specified by the user of the ShinyApp user ID and password and check if there is a match. Message structure:
<-
login_message RegLogConnectorMessage(
type = "login",
username = "Whatever",
password = "&f5*MSYj^niDt=V'3.[dyEX.C/")
str(login_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.082"
#> $ type : chr "login"
#> $ data :List of 2
#> ..$ username: chr "Whatever"
#> ..$ password: chr "&f5*MSYj^niDt=V'3.[dyEX.C/"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
DBI_register_handler
gsheet_register_handler
These functions are handling querying the database and checking if the specified user ID and email for new user aren’t already existing in the database. If there is no conflicts, it will then hash provided password and input new row. Message structure:
<-
register_message RegLogConnectorMessage(
type = "register",
username = "IAmNewThere",
email = "something@new.com",
password = "veryHardP422w0rd!")
str(register_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.104"
#> $ type : chr "register"
#> $ data :List of 3
#> ..$ username: chr "IAmNewThere"
#> ..$ email : chr "something@new.com"
#> ..$ password: chr "veryHardP422w0rd!"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
DBI_credsEdit_handler
gsheet_credsEdit_handler
These functions are querying the database to search for the specified account ID and verify password. After confirming user identity, it can update the database row for this user with any or all of: new username, new email and new password. Message structure:
<-
credsEdit_message RegLogConnectorMessage(
type = "credsEdit",
account_id = 1,
password = "&f5*MSYj^niDt=V'3.[dyEX.C/",
new_username = "Whenever",
new_email = "edited@email.com",
new_password = "veryHardP422w0rd!")
str(credsEdit_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.125"
#> $ type : chr "credsEdit"
#> $ data :List of 5
#> ..$ account_id : num 1
#> ..$ password : chr "&f5*MSYj^niDt=V'3.[dyEX.C/"
#> ..$ new_username: chr "Whenever"
#> ..$ new_email : chr "edited@email.com"
#> ..$ new_password: chr "veryHardP422w0rd!"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
DBI_resetPass_generation_handler
gsheet_resetPass_generation_handler
These functions are querying the database to search for the specified username. After confirming that the specified username exists, it generates and inputs reset code that the user can use to generate new password. Message structure:
<-
resetPass_generate_message RegLogConnectorMessage(
type = "resetPass_generate",
username = "Whatever")
str(resetPass_generate_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.151"
#> $ type : chr "resetPass_generate"
#> $ data :List of 1
#> ..$ username: chr "Whatever"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
DBI_resetPass_confirmation_handler
gsheet_resetPass_confirmation_handler
These functions are querying the database to search for the specified username and confirming that provided reset code is correct. After confirmation, it marks the reset code as used and updates password for the user. Message structure:
<-
resetPass_confirm_message RegLogConnectorMessage(
type = "resetPass_confirm",
username = "Whatever",
reset_code = "4265417643",
password = "veryHardP422w0rd!")
str(resetPass_confirm_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.171"
#> $ type : chr "resetPass_confirm"
#> $ data :List of 3
#> ..$ username : chr "Whatever"
#> ..$ reset_code: chr "4265417643"
#> ..$ password : chr "veryHardP422w0rd!"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
All default handlers for mailConnectors use the same handler functions:
emayili_reglog_mail_handler
gmailr_reglog_mail_handler
They send the email to the specified address using subject and html
body of the email kept in the
mailConnector$mails[[message_type]]
list. They also replace
all of occurences of ?username?, ?email?,
?app_name?, ?app_address? and ?reset_code?
with respective values received in the
RegLogConnectorMessage.
Mail creation is chosen from the mailConnector$mails
public field on the basis of the process
RegLogConnectorMessage value.
<-
resetPass_mail_message RegLogConnectorMessage(
type = "reglog_mail",
process = "resetPass",
username = "Whatever",
email = "edited@email.com",
app_name = "RegLog Nice ShinyApp",
app_address = "https://reglog.nice.com",
reset_code = "4265417643")
str(resetPass_mail_message)
#> List of 4
#> $ time : chr "2022-08-29 22:56:35.192"
#> $ type : chr "reglog_mail"
#> $ data :List of 6
#> ..$ process : chr "resetPass"
#> ..$ username : chr "Whatever"
#> ..$ email : chr "edited@email.com"
#> ..$ app_name : chr "RegLog Nice ShinyApp"
#> ..$ app_address: chr "https://reglog.nice.com"
#> ..$ reset_code : chr "4265417643"
#> $ logcontent: NULL
#> - attr(*, "class")= chr "RegLogConnectorMessage"
There are also provided handlers to send custom e-mails to the logged user.
emayili_custom_mail_handler
gmailr_custom_mail_handler
They are sending email to the specified address parsing it from
provided inside the message subject and body,
providing also an option to send an attachment. No additional
parsing is done there and process
value there is only
informative - it is saved into logs and presented in the
RegLogServer$mail_message()
field.
<- RegLogConnectorMessage(
message_to_send type = "custom_mail",
process = "attachment_mail",
username = "Whatever",
email = "edited@email.com",
mail_subject = "Custom message with attachement",
mail_body = "<p>This is a custom message send from my App</p>
<p>It is completely optional, but that kind of message can also
contain an attachment!</p>",
mail_attachement = "files/myplot.png"
)
Handler function system of dbConnectors and mailConnectors allows for creating custom logic for communicating with them.
For example purposes the custom action that will be described in this vignette will be the process of saving and reading from googlesheet based database results of simple, 10-item questionnaire: Rosenberg’s Self-esteem scale.
Firstly, we need to create another sheet in the googlesheet that is used by the RegLogGsheetConnector to store our data. Besides the summed score we will also need timestamp and username to read the most recent row for the currently logged user.
# create new sheet to the googlesheet
::write_sheet(
googlesheets4ss = gsheet_ss,
sheet = "SES_results",
# append 0-row data.frame to create the "schema" for the sheet
data = data.frame(timestamp = as.character(NA),
user_id = as.character(NA),
score = as.numeric(NA))[-1,]
)
All handler functions need to take as arguments objects
self
, private
and message
and
return RegLogConnectorMessage
.
<- function(self, private, message) {
write_SES_handler
::sheet_append(
googlesheets4# ID of the connected googlesheet is stored inside private of the
# RegLogGsheetConnector
ss = private$gsheet_ss,
sheet = "SES_results",
data = data.frame(
# db_timestamp creates nicely formatted and interpretable by most
# databases current time
timestamp = db_timestamp(),
# user ID and score should be received inside received message
user_id = message$data$user_id,
score = message$data$score
))
return(RegLogConnectorMessage(type = "write_SES",
success = TRUE))
}
As we have now the writing handler ready, we should create a handler to retrieve the data in another user session.
<- function(self, private, message) {
read_SES_handler
# read all results
<- googlesheets4::read_sheet(
SES_results ss = private$gsheet_ss,
sheet = "SES_results",
col_types = "ccn")
# get the lastest result for the current user
<- SES_results |>
SES_results ::filter(user_id == message$data$user_id) |>
dplyr::arrange(dplyr::desc(timestamp)) |>
dplyr::slice_head()
dplyr
# return the RegLogConnectorMessage with the score if available
if (nrow(SES_results) == 1) {
return(RegLogConnectorMessage(type = "read_SES",
success = TRUE,
score = SES_results$score))
else {
} return(RegLogConnectorMessage(type = "read_SES",
success = FALSE))
} }
I will present there only code for the server logic, containing all needed elements for appending created custom handlers and sending the RegLogConnectorMessages to both write and read data from new sheet.
# create and assign RegLogServer object
<- RegLogServer$new(
RegLog # create googlesheet connector
dbConector = RegLogGsheetConnector$new(
# provide correct googlesheet ID
gsheet_ss = gsheet_ss,
# provide handlers in a named list. Names will be used to choose on basis
# of received RegLogConnectorMessage which function to use
custom_handlers = list(write_SES = write_SES_handler,
read_SES = read_SES_handler)
),# provide some mailConnector with all needed data
mailConnector = mailConnector
)
# create an event to write the data to the database: there actionButton will
# trigger it
observeEvent(input$write_ses_result, {
# make sure the inputs are provided
req(input$SES_1, input$SES_2, input$SES_3, input$SES_4, input$SES_5,
$SES_6, input$SES_7, input$SES_8, input$SES_9, input$SES_10)
input
# get the score by summing all raw scores of items
<- sum(input$SES_1, input$SES_2, input$SES_3, input$SES_4, input$SES_5,
score $SES_6, input$SES_7, input$SES_8, input$SES_9, input$SES_10)
input
# send message to the dbConnector's listener
$dbConnector$listener(
RegLogRegLogConnectorMessage(
# specify correct type - the same as the name of the handler
type = "write_SES",
# get required user ID from the RegLog object
user_id = RegLog$user_id(),
score = score))
})
# create an event to read the data from the database: eg. another actionButton
observeEvent(input$read_last_ses_result, {
# send correct message to the dbConnector's listener
$dbConnector$listener(
RegLogRegLogConnectorMessage(
type = "read_SES",
user_id = RegLog$user_id())
)
})
# assign the retrieved data: eg. to the reactive
<- reactive(
SES_result # retrieved data will be available in `message()` field of RegLog object
<- RegLog$message()
received_message # make sure to only process correct type of message
req(received_message$type == "read_SES")
if (!is.null(score)) {
# get the score if there was any saved in the database
$data$score
received_message })