Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ S3method(api_build,op_base_connect)
S3method(api_build,op_head)
S3method(as.data.frame,connect_integration_list)
S3method(as.data.frame,connect_list_hits)
S3method(as.data.frame,connect_users)
S3method(as.data.frame,tbl_connect)
S3method(as_tibble,connect_integration_list)
S3method(as_tibble,connect_list_hits)
S3method(as_tibble,connect_users)
S3method(connect_vars,op_base)
S3method(connect_vars,op_single)
S3method(connect_vars,tbl_connect)
Expand Down Expand Up @@ -115,6 +117,7 @@ export(get_usage_shiny)
export(get_usage_static)
export(get_user_permission)
export(get_users)
export(get_users_list)
export(get_vanity_url)
export(get_vanity_urls)
export(get_variant)
Expand Down
36 changes: 23 additions & 13 deletions R/connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,10 @@ Connect <- R6::R6Class(
# users -----------------------------------------------

#' @description Get user details.
#' @param guid The user GUID.
#' @param guid The user GUID or a `connect_user` object.
user = function(guid) {
self$GET(v1_url("users", guid))
guid <- get_user_guid(guid)
prepend_class(self$GET(v1_url("users", guid)), "connect_user")
},

#' @description Get users.
Expand Down Expand Up @@ -527,7 +528,11 @@ Connect <- R6::R6Class(
user_role = user_role,
account_status = account_status
)
self$GET(path, query = query)
res <- self$GET(path, query = query)
if (!is.null(res$results)) {
res$results <- lapply(res$results, prepend_class, "connect_user")
}
res
},

#' @description Get remote users.
Expand Down Expand Up @@ -585,8 +590,9 @@ Connect <- R6::R6Class(
},

#' @description Lock a user.
#' @param user_guid User GUID.
users_lock = function(user_guid) {
#' @param user User GUID or a `connect_user` object.
users_lock = function(user) {
user_guid <- get_user_guid(user)
Comment on lines +594 to +595
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking ahead in this line of work to functions that act on a single user. We'd like to be able to something like lock_user(user), without having to also pass in the client as the first argument.

Different objects in the package achieve this in different ways. The R6 classes have the client as a property. The jobs (just a list, no class) have the client as an added list item. Integrations (an S3 class, which is the most similar to what we're doing here) have the client as an attribute — the thing I like about that is that it keeps the list fields as the data returned by the server, which feels neat and tidy to me.

That doesn't need to happen in this PR — it looks like most of the user-facing functions that are being updated are ones that also operate on content, and so already have access to a client.

path <- v1_url("users", user_guid, "lock")
message(path)
self$POST(
Expand All @@ -596,8 +602,9 @@ Connect <- R6::R6Class(
},

#' @description Unlock a user.
#' @param user_guid User GUID.
users_unlock = function(user_guid) {
#' @param user User GUID or a `connect_user` object.
users_unlock = function(user) {
user_guid <- get_user_guid(user)
path <- v1_url("users", user_guid, "lock")
self$POST(
path = path,
Expand All @@ -606,9 +613,10 @@ Connect <- R6::R6Class(
},

#' @description Update a user.
#' @param user_guid User GUID.
#' @param user User GUID or a `connect_user` object.
#' @param ... User fields.
users_update = function(user_guid, ...) {
users_update = function(user, ...) {
user_guid <- get_user_guid(user)
path <- v1_url("users", user_guid)
self$PUT(
path = path,
Expand Down Expand Up @@ -641,16 +649,18 @@ Connect <- R6::R6Class(

#' @description Add a group member.
#' @param group_guid The group GUID.
#' @param user_guid The user GUID.
group_member_add = function(group_guid, user_guid) {
#' @param user The user GUID or a `connect_user` object.
group_member_add = function(group_guid, user) {
user_guid <- get_user_guid(user)
path <- v1_url("groups", group_guid, "members")
self$POST(path, body = list(user_guid = user_guid))
},

#' @description Remove a group member.
#' @param group_guid The group GUID.
#' @param user_guid The user GUID.
group_member_remove = function(group_guid, user_guid) {
#' @param user The user GUID or a `connect_user` object.
group_member_remove = function(group_guid, user) {
user_guid <- get_user_guid(user)
path <- v1_url("groups", group_guid, "members", user_guid)
self$DELETE(path)
},
Expand Down
40 changes: 26 additions & 14 deletions R/content.R
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,14 @@ Content <- R6::R6Class(
self$connect$GET(url)
},
#' @description Add a principal to the ACL for this content.
#' @param principal_guid GUID for the target user or group.
#' @param principal_guid GUID for the target user or group. When
#' `principal_type = "user"`, can also be a `connect_user` object.
#' @param principal_type Acting on user or group.
#' @param role The kind of content access.
permissions_add = function(principal_guid, principal_type, role) {
if (principal_type == "user") {
principal_guid <- get_user_guid(principal_guid)
}
url <- v1_url("content", self$content$guid, "permissions")
self$connect$POST(
url,
Expand All @@ -235,10 +239,14 @@ Content <- R6::R6Class(
},
#' @description Alter a principal in the ACL for this content.
#' @param id The target identifier.
#' @param principal_guid GUID for the target user or group.
#' @param principal_guid GUID for the target user or group. When
#' `principal_type = "user"`, can also be a `connect_user` object.
#' @param principal_type Acting on user or group.
#' @param role The kind of content access.
permissions_update = function(id, principal_guid, principal_type, role) {
if (principal_type == "user") {
principal_guid <- get_user_guid(principal_guid)
}
url <- v1_url("content", self$content$guid, "permissions", id)
self$connect$PUT(
url,
Expand Down Expand Up @@ -954,7 +962,6 @@ set_run_as <- function(content, run_as, run_as_current_user = FALSE) {
return(content)
}


#' Delete Content
#'
#' Delete a content item. WARNING: This action deletes all history, configuration,
Expand Down Expand Up @@ -1007,8 +1014,8 @@ content_delete <- function(content, force = FALSE) {
#' @param content An R6 content item
#' @param ... Settings up update that are passed along to Posit Connect
#' @param access_type One of "all", "logged_in", or "acl"
#' @param owner_guid The GUID of a user who is a publisher, so that they can
#' become the new owner of the content
#' @param owner The GUID of a user who is a publisher, so that they can
#' become the new owner of the content. Can also be a `connect_user` object.
#'
#' @return An R6 content item
#'
Expand Down Expand Up @@ -1040,7 +1047,8 @@ content_update_access_type <- function(

#' @rdname content_update
#' @export
content_update_owner <- function(content, owner_guid) {
content_update_owner <- function(content, owner) {
owner_guid <- get_user_guid(owner)
content_update(content = content, owner_guid = owner_guid)
}

Expand Down Expand Up @@ -1103,7 +1111,6 @@ unlock_content <- function(content) {
return(content)
}


#' Verify Content Name
#'
#' Ensures that a content name fits the specifications / requirements of Posit
Expand Down Expand Up @@ -1178,7 +1185,6 @@ delete_bundle <- function(content, bundle_id) {
return(content)
}


#' Content permissions
#'
#' Get or set content permissions for a content item
Expand All @@ -1199,8 +1205,12 @@ delete_bundle <- function(content, bundle_id) {
#' This makes it easier to find / isolate this record.
#'
#' @param content An R6 content object
#' @param guid The guid associated with either a user (for `content_add_user`) or group (for `content_add_group`)
#' @param role The role to assign to a user. Either "viewer" or "owner." Defaults to "viewer"
#' @param user The guid associated with either a user (for `content_add_user`)
#' or group (for `content_add_group`). Can also be a list of `connect_user`
#' objects.
#' @param guid The guid associated with a group.
#' @param role The role to assign to a user. Either "viewer" or "owner."
#' Defaults to "viewer"
#' @param add_owner Optional. Whether to include the owner in returned
#' permission sets. Default is TRUE. The owner will have an NA_character_
#' permission "id"
Expand All @@ -1209,10 +1219,11 @@ delete_bundle <- function(content, bundle_id) {
#' @rdname permissions
#' @family content functions
#' @export
content_add_user <- function(content, guid, role = c("viewer", "owner")) {
content_add_user <- function(content, user, role = c("viewer", "owner")) {
validate_R6_class(content, "Content")
role <- .define_role(role)

guid <- purrr::map_chr(user, get_user_guid)
purrr::map(guid, ~ .content_add_permission_impl(content, "user", .x, role))

return(content)
Expand Down Expand Up @@ -1278,8 +1289,9 @@ content_add_group <- function(content, guid, role = c("viewer", "owner")) {

#' @rdname permissions
#' @export
content_delete_user <- function(content, guid) {
content_delete_user <- function(content, user) {
validate_R6_class(content, "Content")
guid <- purrr::map_chr(user, get_user_guid)
purrr::map(
guid,
~ .content_delete_permission_impl(
Expand Down Expand Up @@ -1331,8 +1343,9 @@ content_delete_group <- function(content, guid) {

#' @rdname permissions
#' @export
get_user_permission <- function(content, guid, add_owner = TRUE) {
get_user_permission <- function(content, user, add_owner = TRUE) {
validate_R6_class(content, "Content")
guid <- get_user_guid(user)
res <- .get_permission(content, "user", guid, add_owner = add_owner)
if (length(res) > 0) {
return(res[[1]])
Expand Down Expand Up @@ -1361,7 +1374,6 @@ get_group_permission <- function(content, guid) {
}
}


#' @rdname permissions
#' @export
get_content_permissions <- function(content, add_owner = TRUE) {
Expand Down
52 changes: 46 additions & 6 deletions R/get.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
#' value (boolean OR). When `NULL` (the default), results are not filtered.

#'
#' @return
#' A tibble with the following columns:
#' @return For `get_users_list`, a list of objects of type `"connect_user"` which
#' contain the following information:
#'
#' * `email`: The user's email
#' * `username`: The user's username
Expand All @@ -33,6 +33,10 @@
#' * `locked`: Whether or not the user is locked
#' * `guid`: The user's GUID, or unique identifier, in UUID RFC4122 format
#'
#' For `get_users`, a data frame with the same fields as `get_users_list`.
#'
#' For `get_user`, a single `"connect_user"` object.
#'
#' @details
#' Please see https://docs.posit.co/connect/api/#get-/v1/users for more information.
#'
Expand All @@ -41,18 +45,41 @@
#' library(connectapi)
#' client <- connect()
#'
#' # Get all users
#' # Get all users as a data frame
#' get_users(client)
#'
#' # Get all licensed users
#' get_users(client, account_status = "licensed")
#'
#' # Get all users who are administrators or publishers
#' get_users(client, user_role = c("administrator", "publisher"))
#'
#' # Get users as a list
#' users_list <- get_users_list(client)
#' }
#'
#' @export
get_users <- function(
get_users <- function(src,
page_size = 500,
prefix = NULL,
limit = Inf,
user_role = NULL,
account_status = NULL) {
as.data.frame(
get_users_list(
src,
page_size,
prefix,
limit,
user_role,
account_status
)
)
}

#' @rdname get_users
#' @export
get_users_list <- function(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Naming! @karawoo and I talked about this yesterday and there are a few options available, including what's used here, just calling it users(), etc.

I was thinking back to a similar discussion with @jonkeane at the start of the year about job-related functions, particularly during #356 (the comment threads are hard to follow and not particularly worth mining).

If we wanted to follow the pattern used with get_job_list() exactly, we'd call this get_user_list() (singular). I think in the abstract I prefer get_users_list(), but if we have a _list() naming scheme, I think it should be consistent in the part of speech used.

I also think it would be fine to call it users().

Calling it get_user_list() feels like a choice that prioritizes clarity in a mixed-paradigm period where it coexists alongside get_users() (which returns a data frame).

Calling it users() would be more confusing when in a mixed-paradigm period ("How is it different from get_users()?"), but would be much nicer for people using the newer object-based system. Perhaps deprecating get_users() would help clarify that the new way to do things is call users() |> as_tibble().

src,
page_size = 500,
prefix = NULL,
Expand All @@ -72,10 +99,23 @@ get_users <- function(
),
limit = limit
)
return(prepend_class(res, "connect_users"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a note — the list-of-integrations object's class is connect_integration_list (see here).

Not saying that that's a better paradigm, and these class names are internal — there isn't much sunk cost here, so if connect_{object_plural} feels better and more comprehensible to future code-readers, we could do that.

}

out <- parse_connectapi_typed(res, connectapi_ptypes$users)
#' @param guid user GUID
#' @rdname get_users
get_user <- function(src, guid) {
src$user(guid)
}

return(out)
#' @export
as.data.frame.connect_users <- function(x, ...) {
parse_connectapi_typed(x, connectapi_ptypes$users)
}

#' @export
as_tibble.connect_users <- function(x, ...) {
parse_connectapi_typed(x, connectapi_ptypes$users)
}

#' Get information about content on the Posit Connect server
Expand Down
25 changes: 25 additions & 0 deletions R/user.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,28 @@ user_guid_from_username <- function(client, username) {
return(res[[1]]$guid)
}
}

#' Extract User GUID
#'
#' Helper function to extract a user GUID from either a character string or a
#' `connect_user` object.
#'
#' @param user Either a character string containing a user GUID or a
#' `connect_user` object (as returned by `Connect$user()` or `Connect$users()`)
#'
#' @return A character string containing the user GUID
#'
#' @keywords internal
Comment on lines +36 to +46
Copy link
Collaborator

Choose a reason for hiding this comment

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

💟

get_user_guid <- function(user) {
if (is.character(user)) {
return(user)
} else if (inherits(user, "connect_user")) {
if (!is.null(user$guid)) {
return(user$guid)
} else {
stop("connect_user object does not contain a guid field")
}
} else {
stop("user must be either a character string (GUID) or a connect_user object")
}
}
6 changes: 6 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,9 @@ message_if_not_testing <- function(...) {
message(...)
}
}

# Prepends a new class to a given objec
prepend_class <- function(x, class) {
class(x) <- c(class, class(x))
x
}
6 changes: 4 additions & 2 deletions man/Content.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading