ropensci / ruODK

ruODK: An R Client for the ODK Central API
https://docs.ropensci.org/ruODK/
GNU General Public License v3.0
42 stars 13 forks source link

QR Code in R #90

Open dmenne opened 4 years ago

dmenne commented 4 years ago

The example below shows how to create QR Codes for test submissions. I post it here because there were are some missing parts in the documentation (base64 encoding), and there was a nasty gotcha in qrencode that stumbles over the allowed CRLF in base64 encoding. I have not tested it exactly as it is, because it is part of a R6 class that creates the warehouse SQLite database.

Some ideas from here

Maybe one day I will try to create a pull request - no now.

  #' @description
  #' Generates draft QR Code.
  #' \url{https://docs.getodk.org/collect-import-export/#list-of-keys-for-all-settings}
  #'
  #' @param xmlFormId The id of this Form as given in its XForms XML
  #' @param draftToken as obtained from `ru_draft_form_details`
  #' @param show_image Displays the QR-Code with image
  #' @param JSON file in inst/extdata as template for settings. Use **
  #' to delimit `draftToken` insert location.
  #' @return QR code for display with `image`
  ru_qr_code = function(xmlFormId, draftToken, pid,
                        show_image = FALSE,
                        collect_json = "collect_settings.json") {
    # Settings file must use ** to delimit replaceable tokens  to avoid
    # collisions with braces in json
    settings_file =
      system.file("extdata", collect_json, package = "sgformulare")
    if (!file.exists(settings_file))
      settings_file = rprojroot::find_package_root_file("inst", "extdata", collect_json)
    if (!file.exists(settings_file))
      stop("Settings file ", settings_file, " not found")
    url = ruODK::get_default_url()
    server_url = glue::glue(
      "{url}/v1/test/{draftToken}/projects/{pid}/forms/{xmlFormId}/draft")
    # Read settings with ** as glue delimiter
    settings = gsub("[\r\n\t ]", "",  readr::read_file(settings_file))
    qq = glue::glue(settings, .open = "**", .close = "**")

    # Linebreaks must be removed. qrencode does not follow standards here
    # https://github.com/jeroen/jsonlite/issues/220
    q64 = gsub("[\r\n]", "", jsonlite::base64_enc(memCompress(qq, "gzip")))
    qr = qrencoder::qrencode_raw(q64)
    if (show_image) {
      # pp = par(mar = c(0,0,0,0) )
      image(qr,  asp = 1, col = c("white", "black"), axes = FALSE, xlab = "", ylab = "")
      # par(pp)
    }
    qr
  }

Template collect_settings.json

{
    "admin": {
        "view_sent": false,
        "change_server": false,
        "analytics": false,
        "save_as": false
    },

    "general": {
        "server_url": "**server_url**",
        "navigation": "swipe_buttons",
        "periodic_form_updates_check": "every_fifteen_minutes",
        "autosend": "wifi_only",
        "guidance_hint": "yes_collapsed",
        "analytics": false
    }
}
florianm commented 4 years ago

Thanks heaps for the implementation! Sounds like a good addition. I won't have bandwidth for the next two weeks but happy to work on this!

florianm commented 3 years ago

I'm revisiting this issue and am thinking that another great use for the function is to bake settings into the QR code, something that ODK Central doesn't yet do. In my day job every app user has different settings (e.g. auto send or not, theme, map base layers).

So this function could be complemented by a vignette "qr" explaining how to choose and write settings into one or several settings.json, then create a QR code for a given project using those settings files.

dmenne commented 3 years ago

One caveat: I do not know how much free space is left in the compressed 64*64 code. However, it could be that other sizes are possible.

Slightly different QR-Code for app users


 # Higher level functions using REST API --------------------

  #' @description
  #' Returns App user qr code
  #' \url{https://docs.getodk.org/collect-import-export/#list-of-keys-for-all-settings}
  #'
  #' @param project_id The project ID
  #' @param user_id The id of the user, e.g. 115
  #' @param show_image Displays the QR-Code with image
  #' @param collect_json JSON file in inst/extdata as template for settings. Use **
  #' to delimit `draft_token` insert location.
  #' @return QR code for display with `image`
  #' @export
  app_user_qr_code = function(project_id,
                              user_id,
                              show_image = FALSE,
                              collect_json = "app_user_settings.json") {
    app_users = self$list_app_users(project_id)$content
    if (!is.null(app_users$message)) 
      stop("app_user_qr_code: Invalid project id")
    if (!(user_id %in% app_users$id))
        stop("app_user_qr_code: Invalid user id")
    key = app_users$token # Do not remove, used in glue
    # Settings file must use ** to delimit replaceable tokens  to avoid
    # collisions with braces in json
    settings_file =  private$get_extdata_file(collect_json)
    stopifnot(file.exists(settings_file))
    # Read settings with ** as glue delimiter
    # TODO : remove readr!
    settings = gsub("[\r\n\t ]", "",  readr::read_file(settings_file))
    qq = glue::glue(settings, .open = "**", .close = "**")

    # Line breaks must be removed. qrencode does not follow standards here
    # https://github.com/jeroen/jsonlite/issues/220
    q64 = gsub("[\r\n]", "", jsonlite::base64_enc(memCompress(qq, "gzip")))
    qr = qrencoder::qrencode_raw(q64)
    if (show_image) {
      # pp = par(mar = c(0,0,0,0) )
      image(qr,  asp = 1, col = c("white", "black"), axes = FALSE,  
            useRaster = TRUE,
            xlab = glue::glue(
              "Draft: Project ({project_id}), user_id: {user_id}"), ylab = "")
      # par(pp)
    }
    invisible(qr)
  }
  )
florianm commented 1 year ago

Reviving Dieter's neat idea with the options to add more settings to the QR code than Central currently exports, and to add a plaintext version of the QR content (app user name, maybe some settings) to the exported QR code.

Thanks to @ivangayton for suggesting the Python package as alternative https://pypi.org/project/segno/