Skip to content

Commit 9af9f15

Browse files
committed
Switch from sheets to google-sheets4
This API preserves the `Option`ality of returned fields, which is useful in determining the type of a field.
1 parent 8a95435 commit 9af9f15

File tree

9 files changed

+629
-329
lines changed

9 files changed

+629
-329
lines changed

Cargo.lock

Lines changed: 251 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dotenv = "0.15.0"
1616
email_address = { git = "https://github.com/illicitonion/rust-email_address.git", rev = "12cd9762a166b79a227beaa90b2f60a768d7c55c" }
1717
futures = "0.3.31"
1818
google-drive = "0.7.0"
19+
google-sheets4 = "6.0.0"
1920
gsuite-api = "0.7.0"
2021
http = "1.3.1"
2122
http-serde = "2.1.1"

src/auth.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use axum::{
66
};
77
use http::Uri;
88
use serde::Deserialize;
9-
use sheets::Client;
109
use tower_sessions::Session;
1110
use uuid::Uuid;
1211

@@ -112,7 +111,8 @@ pub async fn handle_google_oauth_callback(
112111
server_state.config.public_base_url
113112
);
114113

115-
let mut client = Client::new(
114+
// TODO: Replace this with some other way of getting the token (either a request ourselves, or using another library) so we can drop the sheets dep.
115+
let mut client = ::sheets::Client::new(
116116
server_state.config.google_apis_client_id.clone(),
117117
(*server_state.config.google_apis_client_secret).clone(),
118118
redirect_uri,

src/github_accounts.rs

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ use std::collections::BTreeMap;
33
use anyhow::Context;
44
use email_address::EmailAddress;
55
use serde::{Deserialize, Serialize};
6-
use sheets::types::Sheet;
76

87
use crate::{
98
Error,
109
newtypes::{GithubLogin, Region, new_case_insensitive_email_address},
11-
sheets::{SheetsClient, cell_string},
10+
sheets::{Sheet, SheetsClient, cell_string},
1211
};
1312

1413
// TODO: Replace this with a serde implementation from a Google Sheet.
@@ -17,32 +16,19 @@ pub(crate) async fn get_trainees(
1716
sheet_id: &str,
1817
) -> Result<BTreeMap<GithubLogin, Trainee>, Error> {
1918
const EXPECTED_SHEET_NAME: &str = "Form responses 1";
20-
let data = client.get(sheet_id, true, &[]).await.map_err(|err| {
19+
let data = client.get(sheet_id).await.map_err(|err| {
2120
err.with_context(|| {
2221
format!(
2322
"Failed to get trainees github accounts sheet with id {}",
2423
sheet_id
2524
)
2625
})
2726
})?;
28-
let sheet = data.body.sheets.into_iter().find(|sheet| {
29-
if let Some(properties) = &sheet.properties {
30-
properties.title == EXPECTED_SHEET_NAME
31-
} else {
32-
false
33-
}
34-
});
27+
let sheet = data.get(EXPECTED_SHEET_NAME);
3528
if let Some(sheet) = sheet {
3629
let data = trainees_from_sheet(&sheet).map_err(|err| {
3730
err.with_context(|| {
38-
format!(
39-
"Failed to read trainees from sheet {}",
40-
sheet
41-
.properties
42-
.map(|properties| properties.title)
43-
.as_deref()
44-
.unwrap_or("<unknown>")
45-
)
31+
format!("Failed to read trainees from sheet {}", EXPECTED_SHEET_NAME,)
4632
})
4733
})?;
4834
Ok(data)
@@ -65,41 +51,31 @@ pub struct Trainee {
6551

6652
fn trainees_from_sheet(sheet: &Sheet) -> Result<BTreeMap<GithubLogin, Trainee>, Error> {
6753
let mut trainees = BTreeMap::new();
68-
for data in &sheet.data {
69-
if data.start_column != 0 || data.start_row != 0 {
54+
for (row_index, cells) in sheet.rows.iter().enumerate() {
55+
if row_index == 0 {
56+
continue;
57+
}
58+
if cells.len() < 5 {
7059
return Err(Error::Fatal(anyhow::anyhow!(
71-
"Reading data from Google Sheets API - got data chunk that didn't start at row=0,column=0 - got row={},column={}",
72-
data.start_row,
73-
data.start_column
60+
"Reading trainee data from Google Sheets API, row {} didn't have at least 5 columns",
61+
row_index
7462
)));
7563
}
76-
for (row_index, row) in data.row_data.iter().enumerate() {
77-
if row_index == 0 {
78-
continue;
79-
}
80-
let cells = &row.values;
81-
if cells.len() < 5 {
82-
return Err(Error::Fatal(anyhow::anyhow!(
83-
"Reading trainee data from Google Sheets API, row {} didn't have at least 5 columns",
84-
row_index
85-
)));
86-
}
8764

88-
let github_login = GithubLogin::from(cell_string(&cells[3]));
65+
let github_login = GithubLogin::from(cell_string(&cells[3]));
8966

90-
let email = cell_string(&cells[4]);
67+
let email = cell_string(&cells[4]);
9168

92-
trainees.insert(
93-
github_login.clone(),
94-
Trainee {
95-
name: cell_string(&cells[1]),
96-
region: Region(cell_string(&cells[2])),
97-
github_login,
98-
email: new_case_insensitive_email_address(&email)
99-
.with_context(|| format!("Failed to parse trainee email {}", email))?,
100-
},
101-
);
102-
}
69+
trainees.insert(
70+
github_login.clone(),
71+
Trainee {
72+
name: cell_string(&cells[1]),
73+
region: Region(cell_string(&cells[2])),
74+
github_login,
75+
email: new_case_insensitive_email_address(&email)
76+
.with_context(|| format!("Failed to parse trainee email {}", email))?,
77+
},
78+
);
10379
}
10480

10581
Ok(trainees)

src/mentoring.rs

Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::collections::{BTreeMap, btree_map::Entry};
22

33
use anyhow::Context;
44
use chrono::{NaiveDate, Utc};
5+
use google_sheets4::api::CellData;
56
use serde::Serialize;
6-
use sheets::types::GridData;
77
use tracing::warn;
88

99
use crate::{
@@ -44,55 +44,49 @@ pub async fn get_mentoring_records(
4444
records: BTreeMap::new(),
4545
};
4646

47-
for sheet_data in sheet_data {
48-
if sheet_data.start_column != 0 || sheet_data.start_row != 0 {
49-
return Err(Error::Fatal(anyhow::anyhow!(
50-
"Start column and row were {} and {}, expected 0 and 0",
51-
sheet_data.start_column,
52-
sheet_data.start_row
53-
)));
47+
for (row_number, cells) in sheet_data.into_iter().enumerate() {
48+
if cells.is_empty() {
49+
continue;
5450
}
55-
56-
for (row_number, row) in sheet_data.row_data.into_iter().enumerate() {
57-
let cells = row.values;
58-
if cells.is_empty() {
59-
continue;
51+
if cells.len() < 6 && !cell_string(&cells[0]).is_empty() {
52+
warn!(
53+
"Parsing mentoring data from Google Sheet with ID {}: Not enough columns for row {} - expected at least 6, got {} containing: {}",
54+
mentoring_records_sheet_id,
55+
row_number,
56+
cells.len(),
57+
format!("{:#?}", cells),
58+
);
59+
continue;
60+
}
61+
if row_number == 0 {
62+
let headings = cells.iter().take(6).map(cell_string).collect::<Vec<_>>();
63+
if headings != ["Name", "Region", "Date", "Staff", "Status", "Notes"] {
64+
return Err(Error::Fatal(anyhow::anyhow!(
65+
"Mentoring data sheet contained wrong headings: {}",
66+
headings.join(", ")
67+
)));
6068
}
61-
if cells.len() < 6 && !cell_string(&cells[0]).is_empty() {
62-
warn!(
63-
"Parsing mentoring data from Google Sheet with ID {}: Not enough columns for row {} - expected at least 6, got {} containing: {}",
64-
mentoring_records_sheet_id,
65-
row_number,
66-
cells.len(),
67-
format!("{:#?}", cells),
68-
);
69-
continue;
69+
} else {
70+
if cells[0].effective_value.is_none() {
71+
break;
7072
}
71-
if row_number == 0 {
72-
let headings = cells.iter().take(6).map(cell_string).collect::<Vec<_>>();
73-
if headings != ["Name", "Region", "Date", "Staff", "Status", "Notes"] {
74-
return Err(Error::Fatal(anyhow::anyhow!(
75-
"Mentoring data sheet contained wrong headings: {}",
76-
headings.join(", ")
77-
)));
78-
}
79-
} else {
80-
if cells[0].effective_value.is_none() {
81-
break;
73+
let name = cell_string(&cells[0]);
74+
let date = cell_date(&cells[2]).with_context(|| {
75+
format!(
76+
"Failed to parse date from row {} in sheet ID {}",
77+
row_number + 1,
78+
mentoring_records_sheet_id
79+
)
80+
})?;
81+
let entry = mentoring_records.records.entry(name);
82+
match entry {
83+
Entry::Vacant(entry) => {
84+
entry.insert(MentoringRecord { last_date: date });
8285
}
83-
let name = cell_string(&cells[0]);
84-
let date = cell_date(&cells[2])
85-
.with_context(|| format!("Failed to parse date from row {}", row_number + 1))?;
86-
let entry = mentoring_records.records.entry(name);
87-
match entry {
88-
Entry::Vacant(entry) => {
86+
Entry::Occupied(mut entry) => {
87+
if entry.get().last_date < date {
8988
entry.insert(MentoringRecord { last_date: date });
9089
}
91-
Entry::Occupied(mut entry) => {
92-
if entry.get().last_date < date {
93-
entry.insert(MentoringRecord { last_date: date });
94-
}
95-
}
9690
}
9791
}
9892
}
@@ -103,9 +97,10 @@ pub async fn get_mentoring_records(
10397
async fn get_mentoring_records_grid_data(
10498
client: SheetsClient,
10599
mentoring_records_sheet_id: &str,
106-
) -> Result<Vec<GridData>, Error> {
107-
let data_result = client.get(mentoring_records_sheet_id, true, &[]).await;
108-
let data = match data_result {
100+
) -> Result<Vec<Vec<CellData>>, Error> {
101+
let expected_sheet_title = "Feedback";
102+
let data_result = client.get(mentoring_records_sheet_id).await;
103+
let mut data = match data_result {
109104
Ok(data) => data,
110105
Err(Error::PotentiallyIgnorablePermissions(_)) => {
111106
return Ok(Vec::new());
@@ -120,24 +115,12 @@ async fn get_mentoring_records_grid_data(
120115
return Err(err);
121116
}
122117
};
123-
let expected_sheet_title = "Feedback";
124-
let sheet = data
125-
.body
126-
.sheets
127-
.into_iter()
128-
.find(|sheet| {
129-
sheet
130-
.properties
131-
.as_ref()
132-
.map(|properties| properties.title.as_str())
133-
== Some(expected_sheet_title)
134-
})
135-
.ok_or_else(|| {
136-
Error::Fatal(anyhow::anyhow!(
137-
"Couldn't find sheet '{}' in spreadsheet with ID {}",
138-
expected_sheet_title,
139-
mentoring_records_sheet_id
140-
))
141-
})?;
142-
Ok(sheet.data)
118+
let sheet = data.remove(expected_sheet_title).ok_or_else(|| {
119+
Error::Fatal(anyhow::anyhow!(
120+
"Couldn't find sheet '{}' in spreadsheet with ID {}",
121+
expected_sheet_title,
122+
mentoring_records_sheet_id
123+
))
124+
})?;
125+
Ok(sheet.rows)
143126
}

src/prs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,14 @@ pub(crate) async fn fill_in_reviewers(
207207
.collect())
208208
}
209209

210-
#[derive(PartialEq, Eq, Serialize)]
210+
#[derive(Debug, PartialEq, Eq, Serialize)]
211211
pub(crate) enum CheckStatus {
212212
CheckedAndOk,
213213
CheckedAndCheckAgain,
214214
Unchecked,
215215
}
216216

217-
#[derive(PartialEq, Eq, Serialize)]
217+
#[derive(Debug, PartialEq, Eq, Serialize)]
218218
pub(crate) struct ReviewerStaffOnlyDetails {
219219
pub(crate) name: String,
220220
pub(crate) attended_training: bool,

0 commit comments

Comments
 (0)