The following vignette will demonstrate the basic functionality
(i.e., awesome might) of c2z
.
Collections
Your computer desktop reveals your D&D alignment. Similarly, the way you structure your Zotero library through collections reveals your personality. (Chaotic neutral is not using collections, obviously.)
c2z
lets you create nested collections on a
whim, as seen below.
Creating collections
In this example we define a vector of nine elements based on “the
quick brown fox jumps over the lazy dog”. We use the Zotero
function, a wrapper that connects with the Zotero API and combines all
major functions in c2z
, to create nine nested
collections based on the string (i.e., “dog” nested in “lazy” nested in
“the” et cetera). We set library = TRUE
, thereby querying
the Zotero library, and create = TRUE
to create any
collections that does not exist (i.e., all nine collections).
This is a recursive action, so let’s keep the noise at a minimum and
set silent = TRUE
.
Using collections
Now that we have created some collections, we can access the collections in different ways. By default, the function will find the last element in the vector (i.e., “dog”).
# Dog
create.collections$collections |>
dplyr::select(c(key, name, parentCollection))
#> # A tibble: 1 × 3
#> key name parentCollection
#> <chr> <chr> <chr>
#> 1 Y6CZ8PQ9 dog 6DIEZZEJ
However, we often want access all, or some, nested collections. We
can achieve this using two different approaches. 1) using
recursive = TRUE
to access the given element and then
recursively find the nested collections, or 2) using
ancestor = TRUE
and trace the lineage of the specified
collection.
You may use collection.name
(you don’t have several
top-level collections with the same name, do you? If so, the function
will select the newest collection with the specified name), or
collection.key
if you keep track of such things.
c2z
will by default look for collections names
regardless of letter case, and if this is important set
case.insensitive = FALSE
.
All collections
# Find collections
collections <- Zotero(
collection.names = "The",
case.insensitive = TRUE,
recursive = TRUE,
library = TRUE,
silent = TRUE
)
# print collections
collections$collections |>
dplyr::select(c(key, name, parentCollection))
#> # A tibble: 9 × 3
#> key name parentCollection
#> <chr> <chr> <chr>
#> 1 P4527NK3 over ZYKUUCS3
#> 2 N8Y7ANBY brown GE36M75R
#> 3 GE36M75R quick PDX5A66Q
#> 4 Y6CZ8PQ9 dog 6DIEZZEJ
#> 5 6DIEZZEJ lazy 2UZ8D3SF
#> 6 2UZ8D3SF the P4527NK3
#> 7 ZYKUUCS3 jumps INX272R9
#> 8 INX272R9 fox N8Y7ANBY
#> 9 PDX5A66Q The FALSE
Recursive path
# Find collections
recursive <- Zotero(
collection.names = c("The", "quick", "brown"),
recursive = TRUE,
library = TRUE,
silent = TRUE
)
# print collections
recursive$collections |>
dplyr::select(name)
#> # A tibble: 7 × 1
#> name
#> <chr>
#> 1 over
#> 2 brown
#> 3 dog
#> 4 lazy
#> 5 the
#> 6 jumps
#> 7 fox
Items
Okay, so we’ve created a bunch of collections. Kinda cool, I guess? But collections are somewhat pointless without items. Sooo, lets add some items!
Adding items from Cristin
One motivation to develop c2z
is an attempt at
making registering data at Cristin feel useful. The following example of
the Cristin
function will gather all publications
containing the word “cheese”, published since 2020 (it was a good year
for cheese, probably).
The zotero.import
is set to TRUE
,
indicating that the function will use CristinWrangler
to
convert Cristin metadata into an acceptable format for Zotero.
use.identifiers = TRUE
and the function will therefore use
any identifiers (i.e., ISBN or DOI) to augment the metadata. The items
are posted to the Zotero library using Zotero
, where items
are uploaded using the items
argument. The
index = TRUE
, creating an index of the items.
Rather than finicking over h-indexes and impact factors, Norwegians have a weird obsession with NVI (Norwegian Science Index), which is a two-level index where level one publications are ordinary and level two are great (it probably makes sense). However, Cristin has no simple method of filtering out publications that qualify for the two levels.
The specified filter
arguments try to filter out
categories that usually contain publications that are worthy of the NVI
(see all the Cristin categories).
Let’s post the results to the collection “fox”, while using the
argument get.items = FALSE
as not to add any items in the
collection to the Zotero list.
# Filter items
cristin.filter <- c(
"ACADEMICREVIEW",
"ARTICLEJOURNAL",
"ARTICLE",
"ANTHOLOGYACA",
"CHAPTER",
"CHAPTERACADEMIC",
"CHAPTERARTICLE",
"COMMENTARYACA",
"MONOGRAPHACA"
)
# Query Cristin
cristin <- Cristin(
title = "cheese",
published_since = 2020,
published_before = 2021,
filter = cristin.filter,
zotero.import = TRUE,
use.identifiers = TRUE,
silent = FALSE
)
#> Found 15 results
#> Checking whether references are supported. See `CristinSupported()`
#> Looking for missing data
#> Filtered out 9 results
#> Sequential processing. Converting 6 items to Zotero format (began 16.03.2025 - 15:43:38)
#> Duration: 00:01:12.498 (ended 16.03.2025 - 15:44:51)
# Get the fox key
fox <- collections$collections |>
dplyr::filter(name == "fox") |>
dplyr::pull("key")
# Post the items to the collection called fox
post.cristin <- Zotero(
collection.key = fox,
metadata = cristin$results,
library = TRUE,
index = TRUE,
post = TRUE,
post.collections = FALSE,
post.items = TRUE,
silent = TRUE
)
# Select only names in index and print
post.cristin$index |>
dplyr::select(name) |>
print(width = 80)
#> # A tibble: 6 × 1
#> name
#> <chr>
#> 1 Lundberg et al. (2021) Determination of maintenance Jarlsberg® cheese dose to…
#> 2 Olsen et al. (2021) Feeding concentrates with different protein sources to hi…
#> 3 Lundberg et al. (2020) Increased serum osteocalcin levels and vitamin K statu…
#> 4 Henriquez Parodi et al. (2021) Kavli Selling Cheese in a Tube to the World
#> 5 Gaber et al. (2021) Manufacture and characterization of acid-coagulated fresh…
#> 6 Wolka et al. (2021) Soil organic carbon and associated soil properties in Ens…
In this second example we have decided that 2019 was an even better
year for cheese and change the published_since
argument to
2019. The reason why we are doing this is to demonstrate the duplicate
function in Cristin
, which will identify any Cristin
references imported into Zotero if zotero.check = TRUE
. The
function will try to use the zotero
argument to search the
specified collection(s).
# Query Cristin (again)
cristin2 <- Cristin(
title = "cheese",
published_since = 2019,
published_before = 2021,
filter = cristin.filter,
zotero.import = TRUE,
zotero.check = TRUE,
use.identifiers = FALSE,
zotero = post.cristin
)
#> Found 21 results
#> Checking whether references exist in library
#> Removed 6 duplicates
#> Checking whether references are supported. See `CristinSupported()`
#> Looking for missing data
#> Filtered out 9 results
#> Sequential processing. Converting 6 items to Zotero format (began 16.03.2025 - 15:44:53)
#> Duration: 00:00:02.116 (ended 16.03.2025 - 15:44:55)
Adding items from identifiers
Sometimes, it happens to everybody, we’re having a bunch of ISBNs and
DOIs lying around. Luckily, we can easily add them to Zotero using the
Zotero
wrapper. In this case we are adding them to the
collection “quick”.
# ISBN
isbn.items <- c("9780761973836", "9788215048451")
# DOI
doi.items <- c("https://doi.org/10.31234/osf.io/venu6",
"10.1177/1098214010376532")
# Post the items to the collections called quick
identifiers <- Zotero(
collection.names = c("The", "quick"),
isbn = isbn.items,
doi = doi.items,
library = TRUE,
index = TRUE,
post = TRUE,
post.collections = FALSE,
post.items = TRUE,
post.token = FALSE,
silent = TRUE,
get.items = FALSE
)
# Select only names in index and print
identifiers$index |>
dplyr::select(name) |>
print(width = 80)
#> # A tibble: 4 × 1
#> name
#> <chr>
#> 1 Ong-Dean et al. (2011) Challenges and Dilemmas in Implementing Random Assignm…
#> 2 Wijeakumar et al. (2020) Home assessment of visual working memory in pre-scho…
#> 3 Field & Hole (2003) How to design and report experiments
#> 4 Grøholt et al. (2022) Lærebok i barne- og ungdomspsykiatri
Adding items from the Man
When working in academia within a Norwegian context, you will
frequently need to examine the musings of politicians, or their selected
group of researchers. So, let’s add some random white papers and
official reports using ZoteroGov
.
# Combine to a single tibble using dplyr
gov.items <- dplyr::bind_rows(
# Find some random white papers
ZoteroGov(c("26 (2001-2002)", "31 (2014-2015)"), type = "meldst")$data,
# Finds some random official Norwegian reports.
ZoteroGov(c("2014: 4", "2018: 2"), type = "nou")$data
)
# Post the items to the collection called brown
## Fitting given the source
the.man <- Zotero(
collection.names = c("The", "quick", "brown"),
metadata = gov.items,
library = TRUE,
index = TRUE,
post = TRUE,
post.collections = FALSE,
post.items = TRUE,
post.token = FALSE,
silent = TRUE,
get.items = FALSE
)
# Select only names in index and print
the.man$index |>
dplyr::select(name) |>
print(width = 80)
#> # A tibble: 4 × 1
#> name
#> <chr>
#> 1 St.meld. nr. 26 (2001-2002) Bedre kollektivtransport
#> 2 NOU 2014: 4 (2014) Enklere regler – bedre anskaffelser
#> 3 NOU 2018: 2 (2018) Fremtidige kompetansebehov I
#> 4 Meld. St. 31 (2014–2015) Garden som ressurs – marknaden som mål
Adding items from CRAN
Show some love (citations are love, yes?) for the authors that create the packages that you use! (We see you, Hadley Wickham).
Here, we’re using the ZoteroCran
function to collect
metadata from The Comprehensive R Archive Network (CRAN). The
collection.key
is defined as:
The key is “6DIEZZEJ” and the argument recursive = TRUE
,
thus the function is looking for the specified collection (i.e., “lazy”)
and all its children (i.e., “dog”).
It is often useful to link items to several (nested) collections. For instance, you can create the nested collections “project -> CRAN-packages” and link the items to both collections. You can then choose to access all items under “project”, including those in “CRAN-packages”, or just access those filed under the latter.
# Find selected packages
packages <- c(
"dplyr",
"httr",
"jsonlite",
"purrr",
"rvest",
"rlang",
"tibble",
"tidyr",
"tidyselect"
)
# Post the items to the collections called lazy and dog
cran <- Zotero(
collection.key = lazy,
metadata = ZoteroCran(packages)$data,
library = TRUE,
index = TRUE,
recursive = TRUE,
post = TRUE,
post.collections = FALSE,
post.items = TRUE,
post.token = FALSE,
silent = FALSE,
get.items = FALSE
)
#> Searching for collections
#> Found 9 collections
#> The Zotero list contains: 2 collections, 0 items, and 0 attachments
#> Adding 9 items to library using 1 POST request
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> $post.status.items
#> # A tibble: 9 × 2
#> status key
#> <fct> <chr>
#> 1 success 2UZHXDE9
#> 2 success 2LJFGM78
#> 3 success B8CERAIB
#> 4 success FEB6YC86
#> 5 success 7GU66E2M
#> 6 success AS5CYNSX
#> 7 success YZYRLKA2
#> 8 success HNB7946R
#> 9 success NVSIAN7I
#>
#> $post.summary.items
#> # A tibble: 1 × 2
#> status summary
#> <fct> <int>
#> 1 success 9
#>
#>
#> Creating index for items
# What do we got?
# Select only names in index and print
index <- cran$index |>
dplyr::select(name) |>
print(width = 80)
#> # A tibble: 9 × 1
#> name
#> <chr>
#> 1 Wickham (2023a) dplyr: A Grammar of Data Manipulation
#> 2 Wickham (2023b) httr: Tools for Working with URLs and HTTP
#> 3 Ooms (2025) jsonlite: A Simple and Robust JSON Parser and Generator for R
#> 4 Wickham (2025) purrr: Functional Programming Tools
#> 5 Henry (2025) rlang: Functions for Base Types and Core R and 'Tidyverse' Featu…
#> 6 Wickham (2024a) rvest: Easily Harvest (Scrape) Web Pages
#> 7 Müller (2023) tibble: Simple Data Frames
#> 8 Wickham (2024b) tidyr: Tidy Messy Data
#> 9 Henry (2024) tidyselect: Select from a Set of Strings
Copying
There are many situations where we want to copy collections and items (and attachments) between libraries. You may want to copy from a public Zotero library to your own or share your collections with a research group.
The Zotero
function contains a (somewhat convoluted)
method of copying collections and items from a group library to a user
library (and vice versa). However, in this example we’re appending the
group collections to our long line of nested collections, making the
process somewhat messy. Therefore, we split up the process using the
ZoteroCopy
and ZoteroPost
functions. The
change.library
argument will query Zotero
and
alter the location according to specified coordinates (the default
location is the user library as defined in .Renviron
).
Please see the tutorial on how to set up user/group id and API keys.
# Access the group library (defined in .Renviron)
group.library <- Zotero(
user = FALSE,
library = TRUE,
silent = FALSE
)
#> Searching for collections
#> Found 5 collections
#> The Zotero list contains: 5 collections, 0 items, and 0 attachments
#> Searching for all items in library
#> Found 3 items
#> The Zotero list contains: 5 collections, 3 items, and 0 attachments
# Copy the library creating new keys
copy.library <- ZoteroCopy(
zotero = group.library,
change.library = TRUE,
silent = FALSE
)
#> Copying collections
#> Copying items
# Find key for dog
dog <- collections$collections |>
dplyr::filter(name == "dog") |>
dplyr::pull("key")
# Change parent collection of top-level collection to dog
copy.library$collections <- copy.library$collections |>
dplyr::mutate(
parentCollection = dplyr::case_when(
parentCollection == "FALSE" ~ dog,
TRUE ~ parentCollection
)
)
# Copy the collection and items to the user library
post.copy <- ZoteroPost(
Zotero(
collections = copy.library$collections,
items = copy.library$items
),
silent = FALSE,
post.token = TRUE
)
#> Adding 5 collections to library using 1 POST request
#> NULL
#>
#> Adding 3 items to library using 1 POST request
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> $post.status.items
#> # A tibble: 3 × 3
#> error status key
#> <chr> <fct> <chr>
#> 1 Error 409: Collection DQDXE9GT not found failed A88L8M8T
#> 2 Error 409: Collection DQDXE9GT not found failed M3Q65ZPZ
#> 3 Error 409: Collection DQDXE9GT not found failed NPKPIBVY
#>
#> $post.summary.items
#> # A tibble: 1 × 2
#> status summary
#> <fct> <int>
#> 1 failed 3
if (any(nrow(post.copy$items))) {
# Select only names in index and print
ZoteroIndex(post.copy$items) |>
dplyr::select(name) |>
print(width = 80)
}
#> # A tibble: 3 × 1
#> name
#> <chr>
#> 1 Mishra et al. (2023) Exploring Active and Critical Engagement in Human-Robot …
#> 2 Somby & Stalheim (2023) Vygotsky med VR-briller
#> 3 Somby & Vik (2023) Vygotskys defektologi - et perspektiv på inkluderende oppl…
Bibliography
It’s a wrap!
Now it’s time to harvest the bounty that we have created during the steps above. We could create separate bibliographies, but for the sake of simplicity we are mashing it all together. That’s friendship!
The arguments used in Zotero
are somewhat
self-explanatory, however, we are also csl.type
to access a
style
repository in order to create a Citation Style Language (CSL) file
according to APA7.
# Create references.bib in biblatex format with style.csl according to APA7
bibliography <- Zotero(
collection.names = "The",
recursive = TRUE,
library = TRUE,
export = TRUE,
format = "biblatex",
save.data = TRUE,
save.path = tempdir(),
bib.name = "references",
csl.type = "apa-single-spaced",
csl.name = "style",
silent = TRUE
)
# What do we got?
sprintf(
"We now have %s collections, %s references, and %s attachments",
bibliography$n.collections,
bibliography$n.items,
bibliography$n.attachments
)
#> [1] "We now have 9 collections, 23 references, and 0 attachments"
Deleting
Housecleaning! We should clean up the mess in your library. You could
set ragnarok
and force = TRUE
, if you want to delete everything in your
library.
# Delete all collections and items belonging to initial key
delete <- ZoteroDelete(
bibliography,
delete.collections = TRUE,
delete.items = TRUE
)
#> Deleting 9 collections using 1 DELETE request
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> Deleting 23 items using 1 DELETE request
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
References
We can now display the references that we have collected and created
using c2z
.