From e518b679ebfdc0fc811555b0b13f9d83e6c95eba Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 12:08:01 -0400 Subject: [PATCH 1/8] add lock_content and unlock_content --- NAMESPACE | 2 + R/connect.R | 9 +++- R/content.R | 57 ++++++++++++++++++++++++ man/content_delete.Rd | 1 + man/content_item.Rd | 1 + man/content_title.Rd | 1 + man/content_update.Rd | 1 + man/create_random_name.Rd | 1 + man/dashboard_url.Rd | 1 + man/delete_thumbnail.Rd | 1 + man/delete_vanity_url.Rd | 1 + man/deploy_repo.Rd | 1 + man/environment.Rd | 1 + man/get_associations.Rd | 1 + man/get_bundles.Rd | 2 + man/get_image.Rd | 1 + man/get_job.Rd | 1 + man/get_jobs.Rd | 1 + man/get_log.Rd | 1 + man/get_thumbnail.Rd | 1 + man/get_vanity_url.Rd | 1 + man/git.Rd | 1 + man/has_thumbnail.Rd | 1 + man/lock_content.Rd | 79 +++++++++++++++++++++++++++++++++ man/permissions.Rd | 1 + man/search_content.Rd | 1 + man/set_image.Rd | 1 + man/set_integrations.Rd | 1 + man/set_run_as.Rd | 1 + man/set_thumbnail.Rd | 1 + man/set_vanity_url.Rd | 1 + man/swap_vanity_url.Rd | 1 + man/swap_vanity_urls.Rd | 1 + man/terminate_jobs.Rd | 1 + man/verify_content_name.Rd | 1 + tests/integrated/test-content.R | 20 +++++++++ tests/testthat/test-content.R | 28 ++++++++++++ 37 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 man/lock_content.Rd diff --git a/NAMESPACE b/NAMESPACE index 889d578a4..c04ae9b2b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -125,6 +125,7 @@ export(get_variants) export(groups_create_remote) export(has_image) export(has_thumbnail) +export(lock_content) export(page_cursor) export(page_offset) export(poll_task) @@ -163,6 +164,7 @@ export(swap_vanity_url) export(swap_vanity_urls) export(tbl_connect) export(terminate_jobs) +export(unlock_content) export(update_integration) export(user_guid_from_username) export(users_create_remote) diff --git a/R/connect.R b/R/connect.R index 7da0a69fc..3daa3b7f0 100644 --- a/R/connect.R +++ b/R/connect.R @@ -45,7 +45,9 @@ Connect <- R6::R6Class( #' @param server The base URL of your Posit Connect server. #' @param api_key Your Posit Connect API key. initialize = function(server, api_key) { - message_if_not_testing(glue::glue("Defining Connect with server: {server}")) + message_if_not_testing(glue::glue( + "Defining Connect with server: {server}" + )) if (is.null(httr::parse_url(server)$scheme)) { stop(glue::glue( "ERROR: Please provide a protocol (http / https). You gave: {server}" @@ -450,7 +452,10 @@ Connect <- R6::R6Class( include = "tags,owner" ) { if (!is.null(guid)) { - return(self$GET(v1_url("content", guid), query = list(include = include))) + return(self$GET( + v1_url("content", guid), + query = list(include = include) + )) } query <- list( diff --git a/R/content.R b/R/content.R index 4ba1ae6df..572875ad3 100644 --- a/R/content.R +++ b/R/content.R @@ -1051,6 +1051,63 @@ content_update_owner <- function(content, owner_guid) { content_update(content = content, owner_guid = owner_guid) } +#' Lock or Unlock Content +#' +#' Lock or unlock a content item. When content is locked, all processes are +#' terminated, rendering is disabled, and new bundles cannot be deployed. +#' +#' `lock_content()` locks a content item with an optional message displayed to +#' visitors (supports Markdown). +#' +#' `unlock_content()` unlocks a content item, reverting the effects of locking. +#' +#' @param content An R6 content item +#' @param locked_message Optional. A custom message that is displayed by the +#' content item when locked. It is possible to format this message using Markdown. +#' +#' @return An R6 content item +#' +#' @family content functions +#' @rdname lock_content +#' @export +#' @examples +#' \dontrun{ +#' # Lock content with a message +#' client <- connect() +#' content <- content_item(client, "content-guid") +#' content <- lock_content(content, locked_message = "Ah ah ah! You didn't say the magic word!") +#' +#' # Lock content without a message +#' content <- lock_content(content) +#' +#' # Unlock content +#' content <- unlock_content(content) +#' } +lock_content <- function(content, locked_message = NULL) { + validate_R6_class(content, "Content") + + update_params <- list(locked = TRUE) + if (!is.null(locked_message)) { + update_params$locked_message <- locked_message + } + + content$update(!!!update_params) + content$get_content_remote() + + return(content) +} + +#' @rdname lock_content +#' @export +unlock_content <- function(content) { + validate_R6_class(content, "Content") + + content$update(locked = FALSE, locked_message = "") + content$get_content_remote() + + return(content) +} + #' Verify Content Name #' diff --git a/man/content_delete.Rd b/man/content_delete.Rd index 5ee40beb0..dec5f7f5c 100644 --- a/man/content_delete.Rd +++ b/man/content_delete.Rd @@ -39,6 +39,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/content_item.Rd b/man/content_item.Rd index e6398dc56..9f4c472b6 100644 --- a/man/content_item.Rd +++ b/man/content_item.Rd @@ -45,6 +45,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/content_title.Rd b/man/content_title.Rd index 5ddda6901..111098901 100644 --- a/man/content_title.Rd +++ b/man/content_title.Rd @@ -41,6 +41,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/content_update.Rd b/man/content_update.Rd index 02e353730..ef1254e8b 100644 --- a/man/content_update.Rd +++ b/man/content_update.Rd @@ -61,6 +61,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/create_random_name.Rd b/man/create_random_name.Rd index 25915846c..43efee7c2 100644 --- a/man/create_random_name.Rd +++ b/man/create_random_name.Rd @@ -38,6 +38,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/dashboard_url.Rd b/man/dashboard_url.Rd index b9ae6b9da..b1215cb7a 100644 --- a/man/dashboard_url.Rd +++ b/man/dashboard_url.Rd @@ -38,6 +38,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/delete_thumbnail.Rd b/man/delete_thumbnail.Rd index 2c7c0895d..51676a89c 100644 --- a/man/delete_thumbnail.Rd +++ b/man/delete_thumbnail.Rd @@ -49,6 +49,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/delete_vanity_url.Rd b/man/delete_vanity_url.Rd index ad21043cf..884fc0c8f 100644 --- a/man/delete_vanity_url.Rd +++ b/man/delete_vanity_url.Rd @@ -33,6 +33,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/deploy_repo.Rd b/man/deploy_repo.Rd index f1f879b60..ff407cdcd 100644 --- a/man/deploy_repo.Rd +++ b/man/deploy_repo.Rd @@ -76,6 +76,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/environment.Rd b/man/environment.Rd index b4ca8dbba..208f10b2b 100644 --- a/man/environment.Rd +++ b/man/environment.Rd @@ -58,6 +58,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_associations.Rd b/man/get_associations.Rd index c03e8b736..3f71b91ca 100644 --- a/man/get_associations.Rd +++ b/man/get_associations.Rd @@ -75,6 +75,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_bundles.Rd b/man/get_bundles.Rd index 28bdd93f6..1244ebbea 100644 --- a/man/get_bundles.Rd +++ b/man/get_bundles.Rd @@ -38,6 +38,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, @@ -70,6 +71,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_image.Rd b/man/get_image.Rd index 90508762a..ca31f0761 100644 --- a/man/get_image.Rd +++ b/man/get_image.Rd @@ -48,6 +48,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_job.Rd b/man/get_job.Rd index 0d4654ef5..b194499a8 100644 --- a/man/get_job.Rd +++ b/man/get_job.Rd @@ -42,6 +42,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_jobs.Rd b/man/get_jobs.Rd index b7cfd9b39..6d4d79831 100644 --- a/man/get_jobs.Rd +++ b/man/get_jobs.Rd @@ -117,6 +117,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_log.Rd b/man/get_log.Rd index 07ea7540f..7d07cc78d 100644 --- a/man/get_log.Rd +++ b/man/get_log.Rd @@ -64,6 +64,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_thumbnail.Rd b/man/get_thumbnail.Rd index 6063b628d..e0414c37e 100644 --- a/man/get_thumbnail.Rd +++ b/man/get_thumbnail.Rd @@ -54,6 +54,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/get_vanity_url.Rd b/man/get_vanity_url.Rd index 7653ded6b..6cca4576d 100644 --- a/man/get_vanity_url.Rd +++ b/man/get_vanity_url.Rd @@ -36,6 +36,7 @@ Other content functions: \code{\link{get_thumbnail}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/git.Rd b/man/git.Rd index 4173ac67e..3b39581a0 100644 --- a/man/git.Rd +++ b/man/git.Rd @@ -61,6 +61,7 @@ Other content functions: \code{\link{get_thumbnail}()}, \code{\link{get_vanity_url}()}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/has_thumbnail.Rd b/man/has_thumbnail.Rd index cf2b1897e..627042101 100644 --- a/man/has_thumbnail.Rd +++ b/man/has_thumbnail.Rd @@ -50,6 +50,7 @@ Other content functions: \code{\link{get_thumbnail}()}, \code{\link{get_vanity_url}()}, \code{\link{git}}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/lock_content.Rd b/man/lock_content.Rd new file mode 100644 index 000000000..b50140011 --- /dev/null +++ b/man/lock_content.Rd @@ -0,0 +1,79 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/content.R +\name{lock_content} +\alias{lock_content} +\alias{unlock_content} +\title{Lock or Unlock Content} +\usage{ +lock_content(content, locked_message = NULL) + +unlock_content(content) +} +\arguments{ +\item{content}{An R6 content item} + +\item{locked_message}{Optional. A custom message that is displayed by the +content item when locked. It is possible to format this message using Markdown.} +} +\value{ +An R6 content item +} +\description{ +Lock or unlock a content item. When content is locked, all processes are +terminated, rendering is disabled, and new bundles cannot be deployed. +} +\details{ +\code{lock_content()} locks a content item with an optional message displayed to +visitors (supports Markdown). + +\code{unlock_content()} unlocks a content item, reverting the effects of locking. +} +\examples{ +\dontrun{ +# Lock content with a message +client <- connect() +content <- content_item(client, "content-guid") +content <- lock_content(content, locked_message = "Ah ah ah! You didn't say the magic word!") + +# Lock content without a message +content <- lock_content(content) + +# Unlock content +content <- unlock_content(content) +} +} +\seealso{ +Other content functions: +\code{\link{content_delete}()}, +\code{\link{content_item}()}, +\code{\link{content_title}()}, +\code{\link{content_update}()}, +\code{\link{create_random_name}()}, +\code{\link{dashboard_url}()}, +\code{\link{delete_thumbnail}()}, +\code{\link{delete_vanity_url}()}, +\code{\link{deploy_repo}()}, +\code{\link{get_associations}()}, +\code{\link{get_bundles}()}, +\code{\link{get_environment}()}, +\code{\link{get_image}()}, +\code{\link{get_job}()}, +\code{\link{get_jobs}()}, +\code{\link{get_log}()}, +\code{\link{get_thumbnail}()}, +\code{\link{get_vanity_url}()}, +\code{\link{git}}, +\code{\link{has_thumbnail}()}, +\code{\link{permissions}}, +\code{\link{search_content}()}, +\code{\link{set_image_path}()}, +\code{\link{set_integrations}()}, +\code{\link{set_run_as}()}, +\code{\link{set_thumbnail}()}, +\code{\link{set_vanity_url}()}, +\code{\link{swap_vanity_url}()}, +\code{\link{swap_vanity_urls}()}, +\code{\link{terminate_jobs}()}, +\code{\link{verify_content_name}()} +} +\concept{content functions} diff --git a/man/permissions.Rd b/man/permissions.Rd index 40604e2de..1af0aff92 100644 --- a/man/permissions.Rd +++ b/man/permissions.Rd @@ -84,6 +84,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, \code{\link{set_integrations}()}, diff --git a/man/search_content.Rd b/man/search_content.Rd index 6f3881b29..65bd0d895 100644 --- a/man/search_content.Rd +++ b/man/search_content.Rd @@ -185,6 +185,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{set_image_path}()}, \code{\link{set_integrations}()}, diff --git a/man/set_image.Rd b/man/set_image.Rd index 9486f24b7..1f4e485ef 100644 --- a/man/set_image.Rd +++ b/man/set_image.Rd @@ -55,6 +55,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_integrations}()}, diff --git a/man/set_integrations.Rd b/man/set_integrations.Rd index 3ad43cc61..c9a1c906d 100644 --- a/man/set_integrations.Rd +++ b/man/set_integrations.Rd @@ -76,6 +76,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/set_run_as.Rd b/man/set_run_as.Rd index 8485fa3dd..dbf2e6248 100644 --- a/man/set_run_as.Rd +++ b/man/set_run_as.Rd @@ -57,6 +57,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/set_thumbnail.Rd b/man/set_thumbnail.Rd index 79cdab253..b0162279b 100644 --- a/man/set_thumbnail.Rd +++ b/man/set_thumbnail.Rd @@ -54,6 +54,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/set_vanity_url.Rd b/man/set_vanity_url.Rd index 2fd323cc1..dc0d803fb 100644 --- a/man/set_vanity_url.Rd +++ b/man/set_vanity_url.Rd @@ -50,6 +50,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/swap_vanity_url.Rd b/man/swap_vanity_url.Rd index 7d75b43d4..e9e2bc325 100644 --- a/man/swap_vanity_url.Rd +++ b/man/swap_vanity_url.Rd @@ -40,6 +40,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/swap_vanity_urls.Rd b/man/swap_vanity_urls.Rd index 1d69e651d..934349cfb 100644 --- a/man/swap_vanity_urls.Rd +++ b/man/swap_vanity_urls.Rd @@ -39,6 +39,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/terminate_jobs.Rd b/man/terminate_jobs.Rd index 308d77ce9..982670a14 100644 --- a/man/terminate_jobs.Rd +++ b/man/terminate_jobs.Rd @@ -67,6 +67,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/man/verify_content_name.Rd b/man/verify_content_name.Rd index c2f04ce45..fb0b5b0a3 100644 --- a/man/verify_content_name.Rd +++ b/man/verify_content_name.Rd @@ -42,6 +42,7 @@ Other content functions: \code{\link{get_vanity_url}()}, \code{\link{git}}, \code{\link{has_thumbnail}()}, +\code{\link{lock_content}()}, \code{\link{permissions}}, \code{\link{search_content}()}, \code{\link{set_image_path}()}, diff --git a/tests/integrated/test-content.R b/tests/integrated/test-content.R index 4e3c66c3c..952d09396 100644 --- a/tests/integrated/test-content.R +++ b/tests/integrated/test-content.R @@ -337,3 +337,23 @@ test_that("remove a collaborator twice works", { ) expect_false(any(which_match)) }) + +# Lock / Unlock ----------------------------------------- + +test_that("lock and unlock content works", { + tar_path <- rprojroot::find_package_root_file( + "tests/testthat/examples/static.tar.gz" + ) + bund <- bundle_path(path = tar_path) + tsk <- deploy(connect = test_conn_1, bundle = bund) + + # Lock with message + lock_content(tsk, locked_message = "Maintenance in progress") + expect_true(tsk$content$locked) + expect_equal(tsk$content$locked_message, "Maintenance in progress") + + # Unlock + unlock_content(tsk) + expect_false(tsk$content$locked) + expect_equal(tsk$content$locked_message, "") +}) diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index 1197f9b3f..3a08087fd 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -542,3 +542,31 @@ test_that("content search errors on Connect < 2024.04.0", { "ERROR: This feature requires Posit Connect version 2024.04.0 but you are using 2024.03.0." ) }) + +with_mock_dir("2025.09.0", { + test_that("lock_content() and unlock_content() make the expected requests", { + client <- Connect$new( + server = "https://connect.example", + api_key = "not-a-key" + ) + client$version # Hydrate version + item <- content_item(client, "6632a162") + without_internet({ + expect_PATCH( + lock_content(item), + "https://connect.example/__api__/v1/content/6632a162", + '{"locked":true}' + ) + expect_PATCH( + lock_content(item, "ACCESS DENIED"), + "https://connect.example/__api__/v1/content/6632a162", + '{"locked":true,"locked_message":"ACCESS DENIED"}' + ) + expect_PATCH( + unlock_content(item), + "https://connect.example/__api__/v1/content/6632a162", + '{"locked":false,"locked_message":""}' + ) + }) + }) +}) From 8ba6f5b3fedfa1f91b69b24e40289ead137330c9 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 12:26:30 -0400 Subject: [PATCH 2/8] thorough tests --- R/content.R | 2 + .../__api__/v1/content/6632a162-064d19.json | 56 +++++++++++++++++++ tests/testthat/test-content.R | 25 +++++++++ 3 files changed, 83 insertions(+) create mode 100644 tests/testthat/2025.09.0/__api__/v1/content/6632a162-064d19.json diff --git a/R/content.R b/R/content.R index 572875ad3..503159f22 100644 --- a/R/content.R +++ b/R/content.R @@ -1085,6 +1085,7 @@ content_update_owner <- function(content, owner_guid) { #' } lock_content <- function(content, locked_message = NULL) { validate_R6_class(content, "Content") + error_if_less_than(content$connect$version, "2024.08.0") update_params <- list(locked = TRUE) if (!is.null(locked_message)) { @@ -1101,6 +1102,7 @@ lock_content <- function(content, locked_message = NULL) { #' @export unlock_content <- function(content) { validate_R6_class(content, "Content") + error_if_less_than(content$connect$version, "2024.08.0") content$update(locked = FALSE, locked_message = "") content$get_content_remote() diff --git a/tests/testthat/2025.09.0/__api__/v1/content/6632a162-064d19.json b/tests/testthat/2025.09.0/__api__/v1/content/6632a162-064d19.json new file mode 100644 index 000000000..ee29ad946 --- /dev/null +++ b/tests/testthat/2025.09.0/__api__/v1/content/6632a162-064d19.json @@ -0,0 +1,56 @@ +{ + "guid": "6632a162", + "name": "extension-runtime-version-scanner-v1.0.1", + "title": "Runtime Version Scanner", + "description": "See the Python, R, and Quarto versions used by your content. Filter by version, content type, and usage to identify items that need updating. Quickly find content that uses end-of-life language versions.", + "access_type": "acl", + "locked": false, + "locked_message": "", + "connection_timeout": null, + "read_timeout": null, + "init_timeout": null, + "idle_timeout": null, + "max_processes": null, + "min_processes": null, + "max_conns_per_process": null, + "load_factor": null, + "memory_request": null, + "memory_limit": null, + "cpu_request": null, + "cpu_limit": null, + "amd_gpu_limit": null, + "nvidia_gpu_limit": null, + "service_account_name": null, + "default_image_name": null, + "default_environment_guid": null, + "created_time": "2025-10-07T22:02:47Z", + "last_deployed_time": "2025-10-07T22:02:48Z", + "bundle_id": "309503", + "app_mode": "shiny", + "content_category": "", + "parameterized": false, + "environment_guid": null, + "cluster_name": "Local", + "image_name": null, + "r_version": "4.3.3", + "py_version": null, + "quarto_version": null, + "r_environment_management": true, + "default_r_environment_management": null, + "py_environment_management": null, + "default_py_environment_management": null, + "run_as": null, + "run_as_current_user": false, + "owner_guid": "fe07bf64", + "content_url": "https://dogfood.team.pct.posit.it/content/6632a162/", + "dashboard_url": "https://dogfood.team.pct.posit.it/connect/#/apps/6632a162", + "app_role": "owner", + "id": "94876", + "owner": { + "guid": "fe07bf64", + "username": "toph", + "first_name": "Toph", + "last_name": "Allen" + }, + "public_content_status": "unrestricted" +} diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index 3a08087fd..56615bd70 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -543,6 +543,8 @@ test_that("content search errors on Connect < 2024.04.0", { ) }) +# lock content ---- + with_mock_dir("2025.09.0", { test_that("lock_content() and unlock_content() make the expected requests", { client <- Connect$new( @@ -570,3 +572,26 @@ with_mock_dir("2025.09.0", { }) }) }) + +test_that("lock_content() and unlock_content() error on Connect < 2024.08.0", { + client <- MockConnect$new("2024.07.0") + client$mock_response( + "GET", + "v1/content/test-guid", + content = list( + guid = "test-guid", + title = "Test Content", + app_mode = "static" + ) + ) + item <- content_item(client, "test-guid") + expect_error( + lock_content(item), + "ERROR: This feature requires Posit Connect version 2024.08.0 but you are using 2024.07.0." + ) + expect_error( + unlock_content(item), + "ERROR: This feature requires Posit Connect version 2024.08.0 but you are using 2024.07.0." + ) +}) + From a3733a7bb7b15c5311e23b8aed9c291657dec5c8 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 12:30:50 -0400 Subject: [PATCH 3/8] update NEWS.md --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index e06fb0dcd..73c129872 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,8 @@ - Support content search API with the `search_content()` function. (#272) - New `search_content()` function which lets you search and filter content items on the Connect server. (#272, #447) +- New `lock_content()` and `unlock_content()` functions for locking and unlocking + content items. (#453) # connectapi 0.8.0 From b331e2ed31116ecee87fb074969e81cb8da61adc Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 12:42:32 -0400 Subject: [PATCH 4/8] make CI happy --- tests/integrated/test-content.R | 2 ++ tests/testthat/test-content.R | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integrated/test-content.R b/tests/integrated/test-content.R index 952d09396..42eae4c12 100644 --- a/tests/integrated/test-content.R +++ b/tests/integrated/test-content.R @@ -341,6 +341,8 @@ test_that("remove a collaborator twice works", { # Lock / Unlock ----------------------------------------- test_that("lock and unlock content works", { + skip_if_connect_older_than(test_conn_1, "2024.08.0") + tar_path <- rprojroot::find_package_root_file( "tests/testthat/examples/static.tar.gz" ) diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index 56615bd70..8e7b6425a 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -594,4 +594,3 @@ test_that("lock_content() and unlock_content() error on Connect < 2024.08.0", { "ERROR: This feature requires Posit Connect version 2024.08.0 but you are using 2024.07.0." ) }) - From c9cc38d7c5a375f62822fdfe938107e9a97a7a1a Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 16:28:54 -0400 Subject: [PATCH 5/8] Update R/content.R Co-authored-by: Kara Woo --- R/content.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/content.R b/R/content.R index 503159f22..47b312183 100644 --- a/R/content.R +++ b/R/content.R @@ -1083,7 +1083,7 @@ content_update_owner <- function(content, owner_guid) { #' # Unlock content #' content <- unlock_content(content) #' } -lock_content <- function(content, locked_message = NULL) { +lock_content <- function(content, locked_message = "") { validate_R6_class(content, "Content") error_if_less_than(content$connect$version, "2024.08.0") From 6c02ed46294bd62ac0d5b856ff8b797a099bbf42 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 9 Oct 2025 16:38:21 -0400 Subject: [PATCH 6/8] test: verify locking content multiple times with different messages - Test locking content twice with different messages - Test that locking without a message clears existing message --- tests/integrated/test-content.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integrated/test-content.R b/tests/integrated/test-content.R index 42eae4c12..1796c9265 100644 --- a/tests/integrated/test-content.R +++ b/tests/integrated/test-content.R @@ -354,6 +354,16 @@ test_that("lock and unlock content works", { expect_true(tsk$content$locked) expect_equal(tsk$content$locked_message, "Maintenance in progress") + # Lock again with different message + lock_content(tsk, locked_message = "Still under maintenance") + expect_true(tsk$content$locked) + expect_equal(tsk$content$locked_message, "Still under maintenance") + + # Lock again without message clears it + lock_content(tsk) + expect_true(tsk$content$locked) + expect_equal(tsk$content$locked_message, "") + # Unlock unlock_content(tsk) expect_false(tsk$content$locked) From 31a79d6559351201014302e94abd9dabb80a62f1 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 10 Oct 2025 17:44:40 -0400 Subject: [PATCH 7/8] fix failing test --- tests/testthat/test-content.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index 8e7b6425a..1370e87de 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -557,7 +557,7 @@ with_mock_dir("2025.09.0", { expect_PATCH( lock_content(item), "https://connect.example/__api__/v1/content/6632a162", - '{"locked":true}' + '{"locked":true,"locked_message":""}' ) expect_PATCH( lock_content(item, "ACCESS DENIED"), From 097f7cfe034c1722e74eab1d5e08d54e395c718f Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 10 Oct 2025 18:39:29 -0400 Subject: [PATCH 8/8] update docs --- man/lock_content.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/lock_content.Rd b/man/lock_content.Rd index b50140011..858fc5c70 100644 --- a/man/lock_content.Rd +++ b/man/lock_content.Rd @@ -5,7 +5,7 @@ \alias{unlock_content} \title{Lock or Unlock Content} \usage{ -lock_content(content, locked_message = NULL) +lock_content(content, locked_message = "") unlock_content(content) }