Skip to content
Merged
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
1 change: 1 addition & 0 deletions config.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"google_apis_client_secret": "$CYF_TRAINEE_TRACKER_GOOGLE_APIS_CLIENT_SECRET",
"github_email_mapping_sheet_id": "1ahDEnO8odD9oLtO_EBcvcmEaF0qsX-I4iLCIY0XLjt0",
"reviewer_staff_info_sheet_id": "1CKDrXtx5lkgfZ8E2mjvsDup2K8UyBsq99CIV0qOxrP0",
"mentoring_records_sheet_id": "1PYOb__p0nT0vPWPtbbLT0KXGv53xpkNLSCaPFAfRGrk",

"slack_client_id": "85239491699.9205264014663",
"slack_client_secret": "$CYF_TRAINEE_TRACKER_SLACK_CLIENT_SECRET",
Expand Down
6 changes: 6 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub async fn handle_google_oauth_callback(
"{}/api/oauth-callbacks/google-drive",
server_state.config.public_base_url
);

let mut client = Client::new(
server_state.config.google_apis_client_id.clone(),
(*server_state.config.google_apis_client_secret).clone(),
Expand All @@ -119,6 +120,11 @@ pub async fn handle_google_oauth_callback(
.get_access_token(&params.code, params.state.to_string().as_str())
.await
.context("Failed to get access token")?;

if access_token.access_token.is_empty() {
return Err(Error::Fatal(anyhow!("Google gave an empty token")));
}

session
.insert(
auth_state.google_scope.token_session_key(),
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub struct Config {

pub github_email_mapping_sheet_id: String,

pub mentoring_records_sheet_id: String,

pub reviewer_staff_info_sheet_id: String,

// Legacy hack until all trainees are in the sheet.
Expand Down
16 changes: 16 additions & 0 deletions src/course.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
use crate::{
config::CourseScheduleWithRegisterSheetId,
github_accounts::{get_trainees, Trainee},
mentoring::{get_mentoring_records, MentoringRecord},
newtypes::{GithubLogin, Region},
octocrab::all_pages,
prs::{get_prs, Pr, PrState},
Expand Down Expand Up @@ -376,11 +377,18 @@ impl Batch {
.map(|(region, _count)| region)
.collect()
}

pub fn has_mentoring_records(&self) -> bool {
self.trainees
.iter()
.any(|trainee| trainee.mentoring_record.is_some())
}
}

#[derive(Debug)]
pub struct TraineeWithSubmissions {
pub trainee: Trainee,
pub mentoring_record: Option<MentoringRecord>,
pub modules: IndexMap<String, ModuleWithSubmissions>,
}

Expand Down Expand Up @@ -651,6 +659,7 @@ pub async fn get_batch_with_submissions(
octocrab: &Octocrab,
sheets_client: SheetsClient,
github_email_mapping_sheet_id: &str,
mentoring_records_sheet_id: &str,
github_org: &str,
batch_github_slug: &str,
course: &Course,
Expand All @@ -664,6 +673,9 @@ pub async fn get_batch_with_submissions(
)
.await?;

let mentoring_records =
get_mentoring_records(sheets_client.clone(), mentoring_records_sheet_id).await?;

let batch_members = get_batch_members(
octocrab,
sheets_client,
Expand Down Expand Up @@ -734,6 +746,9 @@ pub async fn get_batch_with_submissions(

modules.insert(module_name.clone(), module_with_submissions);
}

let mentoring_record = mentoring_records.get(&trainee_name);

let trainee = TraineeWithSubmissions {
trainee: Trainee {
github_login,
Expand All @@ -744,6 +759,7 @@ pub async fn get_batch_with_submissions(
}),
region,
},
mentoring_record,
modules,
};
trainees.push(trainee);
Expand Down
1 change: 1 addition & 0 deletions src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ pub async fn get_trainee_batch(
&octocrab,
sheets_client,
&server_state.config.github_email_mapping_sheet_id,
&server_state.config.mentoring_records_sheet_id,
github_org,
&batch_github_slug,
&course,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod frontend;
pub mod github_accounts;
pub mod google_auth;
pub mod google_groups;
pub mod mentoring;
pub mod newtypes;
pub mod octocrab;
pub mod prs;
Expand Down
135 changes: 135 additions & 0 deletions src/mentoring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::collections::{btree_map::Entry, BTreeMap};

use anyhow::Context;
use chrono::{NaiveDate, Utc};
use serde::Serialize;
use tracing::warn;

use crate::{
sheets::{cell_date, cell_string, SheetsClient},
Error,
};

pub struct MentoringRecords {
records: BTreeMap<String, MentoringRecord>,
}

impl MentoringRecords {
pub fn get(&self, name: &str) -> Option<MentoringRecord> {
self.records.get(name).cloned()
}
}

#[derive(Clone, Debug, Serialize)]
pub struct MentoringRecord {
pub last_date: NaiveDate,
}

impl MentoringRecord {
pub fn is_recent(&self) -> bool {
let now = Utc::now().date_naive();
let time_since = now.signed_duration_since(self.last_date);
time_since.num_days() <= 14
}
}

pub async fn get_mentoring_records(
client: SheetsClient,
mentoring_records_sheet_id: &str,
) -> Result<MentoringRecords, Error> {
let data = client
.get(mentoring_records_sheet_id, true, &[])
.await
.map_err(|err| {
err.with_context(|| {
format!(
"Failed to get spreadsheet with ID {}",
mentoring_records_sheet_id
)
})
})?;
let expected_sheet_title = "Feedback";
let sheet = data
.body
.sheets
.into_iter()
.find(|sheet| {
sheet
.properties
.as_ref()
.map(|properties| properties.title.as_str())
== Some(expected_sheet_title)
})
.ok_or_else(|| {
Error::Fatal(anyhow::anyhow!(
"Couldn't find sheet '{}' in spreadsheet with ID {}",
expected_sheet_title,
mentoring_records_sheet_id
))
})?;

let mut mentoring_records = MentoringRecords {
records: BTreeMap::new(),
};

for sheet_data in sheet.data {
if sheet_data.start_column != 0 || sheet_data.start_row != 0 {
return Err(Error::Fatal(anyhow::anyhow!(
"Start column and row were {} and {}, expected 0 and 0",
sheet_data.start_column,
sheet_data.start_row
)));
}

for (row_number, row) in sheet_data.row_data.into_iter().enumerate() {
let cells = row.values;
if cells.len() < 6 {
warn!(
"Parsing mentoring data from Google Sheet with ID {}: Not enough columns for row {} - expected at least 6, got {} containing: {}",
mentoring_records_sheet_id,
row_number,
cells.len(),
format!("{:#?}", cells),
);
continue;
}
if row_number == 0 {
let headings = cells
.iter()
.take(6)
.enumerate()
.map(|(col_number, cell)| {
cell_string(cell)
.with_context(|| format!("Failed to get row 0 column {}", col_number))
})
.collect::<Result<Vec<_>, _>>()?;
if headings != ["Name", "Region", "Date", "Staff", "Status", "Notes"] {
return Err(Error::Fatal(anyhow::anyhow!(
"Mentoring data sheet contained wrong headings: {}",
headings.join(", ")
)));
}
} else {
if cells[0].effective_value.is_none() {
break;
}
let name = cell_string(&cells[0])
.with_context(|| format!("Failed to read name from row {}", row_number + 1))?;
let date = cell_date(&cells[2])
.with_context(|| format!("Failed to parse date from row {}", row_number + 1))?;
let entry = mentoring_records.records.entry(name);
match entry {
Entry::Vacant(entry) => {
entry.insert(MentoringRecord { last_date: date });
}
Entry::Occupied(mut entry) => {
if entry.get().last_date < date {
entry.insert(MentoringRecord { last_date: date });
}
}
}
}
}
}
Ok(mentoring_records)
}
6 changes: 6 additions & 0 deletions src/sheets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ pub(crate) fn cell_bool(cell: &CellData) -> Result<bool, anyhow::Error> {
}
}

pub(crate) fn cell_date(cell: &CellData) -> Result<chrono::NaiveDate, anyhow::Error> {
let date_string = &cell.formatted_value;
chrono::NaiveDate::parse_from_str(date_string, "%Y-%m-%d")
.with_context(|| format!("Failed to parse {} as a date", date_string))
}

pub(crate) async fn sheets_client(
session: &Session,
server_state: ServerState,
Expand Down
24 changes: 24 additions & 0 deletions templates/trainee-batch.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
td.pr-unknown {
background-color: grey;
}
td.mentoring-recent {
background-color: var(--green);
}
td.mentoring-stale {
background-color: var(--orange);
}
td.mentoring-unknown {
background-color: grey;
}
.trainee-on-track {
background-color: var(--green);
}
Expand Down Expand Up @@ -89,13 +98,15 @@ <h1>{{ course.name }} - {{ batch.name }}</h1>
<tr>
<th>GitHub</th>
<th>Region</th>
{% if batch.has_mentoring_records() %}<th>Last check-in</th>{% endif %}
{% for (module_name, module) in course.modules %}
<th colspan="{{ module.assignment_count() }}">{{module_name}}</th>
{% endfor %}
</tr>
<tr>
<th></th>
<th></th>
{% if batch.has_mentoring_records() %}<th></th>{% endif %}
{% for (module_name, module) in course.modules %}
{% for (sprint_number, sprint) in module.sprints.iter().enumerate() %}
<th colspan="{{ sprint.assignment_count() }}">Sprint {{ sprint_number + 1 }}</th>
Expand All @@ -105,6 +116,7 @@ <h1>{{ course.name }} - {{ batch.name }}</h1>
<tr>
<th></th>
<th></th>
{% if batch.has_mentoring_records() %}<th></th>{% endif %}
{% for (module_name, module) in course.modules %}
{% for sprint in module.sprints %}
{% for assignment in sprint.assignments %}
Expand All @@ -119,6 +131,18 @@ <h1>{{ course.name }} - {{ batch.name }}</h1>
<tr>
<th class="{{ css_classes_for_trainee_status(&trainee.status()) }}">{{ trainee.trainee.name }} - <a href="https://github.com/{{trainee.trainee.github_login}}">@{{ trainee.trainee.github_login }}</a> - {{ trainee.trainee.email }} - {{ trainee.progress_score() / 100 }}%</th>
<td>{{ trainee.trainee.region }}</td>
{% if batch.has_mentoring_records() %}
{% match trainee.mentoring_record %}
{% when Some(mentoring_record) %}
{% if mentoring_record.is_recent() %}
<td class="mentoring-recent">{{ mentoring_record.last_date }}</td>
{% else %}
<td class="mentoring-stale">{{ mentoring_record.last_date }}</td>
{% endif %}
{% when None %}
<td class="mentoring-unknown">Unknown</td>
{% endmatch %}
{% endif %}
{% for (module_name, module) in trainee.modules %}
{% for sprint in module.sprints %}
{% for submission in sprint.submissions %}
Expand Down