Skip to content

Commit cfa2b01

Browse files
authored
Show optional assignments as optional (#6)
1 parent fa320b6 commit cfa2b01

File tree

3 files changed

+136
-57
lines changed

3 files changed

+136
-57
lines changed

src/course.rs

Lines changed: 128 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ fn parse_issue(issue: &Issue) -> Result<Option<(NonZeroUsize, Option<Assignment>
143143
} = issue;
144144

145145
let mut sprint = None;
146-
let mut assignment = None;
146+
147+
let mut submit_label = None;
148+
let mut optionality = None;
149+
147150
for label in labels {
148151
if let Some(sprint_number) = label.name.strip_prefix("📅 Sprint ") {
149152
if sprint.is_some() {
@@ -164,47 +167,81 @@ fn parse_issue(issue: &Issue) -> Result<Option<(NonZeroUsize, Option<Assignment>
164167
}
165168
}
166169
}
167-
if let Some(submit_label) = label.name.strip_prefix("Submit:") {
168-
if assignment.is_some() {
170+
if let Some(label) = label.name.strip_prefix("Submit:") {
171+
if submit_label.is_some() {
169172
return Err(Error::UserFacing(format!(
170173
"Failed to parse issue {} - duplicate submit labels",
171174
html_url
172175
)));
173176
}
174-
match submit_label {
175-
"None" => {
176-
assignment = Some(None);
177-
}
178-
"PR" => {
179-
assignment = Some(Some(Assignment::ExpectedPullRequest {
180-
title: title.clone(),
181-
}));
182-
}
183-
"Issue" => {
184-
// TODO: Handle these.
185-
assignment = Some(None);
186-
}
187-
other => {
188-
return Err(Error::UserFacing(format!(
189-
"Failed to parse issue {} - submit label wasn't recognised: {}",
190-
html_url, other
191-
)));
192-
}
177+
submit_label = Some(label);
178+
}
179+
180+
if label.name == "🏕 Priority Mandatory" {
181+
if optionality.is_some() {
182+
return Err(Error::UserFacing(format!(
183+
"Failed to parse issue {} - duplicate priority labels",
184+
html_url
185+
)));
186+
}
187+
optionality = Some(AssignmentOptionality::Mandatory)
188+
} else if label.name == "🏝️ Priority Stretch" {
189+
if optionality.is_some() {
190+
return Err(Error::UserFacing(format!(
191+
"Failed to parse issue {} - duplicate priority labels",
192+
html_url
193+
)));
193194
}
195+
optionality = Some(AssignmentOptionality::Stretch)
194196
}
195197
}
198+
196199
let sprint = sprint.ok_or_else(|| {
197200
Error::UserFacing(format!(
198-
"Failed to parse issue {} - no sprint label.\n\nIf this issue was made my a curriculum team member it should be given a sprint label.\nIf this issue was created by a trainee for step submission, it should probably be closed (and they should create the issue in their fork).",
199-
html_url
201+
"Failed to parse issue {} - no sprint label.{}",
202+
html_url, BAD_LABEL_SUFFIX,
203+
))
204+
})?;
205+
206+
let submit_label = match submit_label {
207+
Some(submit_label) => submit_label,
208+
// TODO: For now we treat a missing Submit label as an issue to ignore.
209+
// When all issues have Submit labels, we should error if they're missing.
210+
None => {
211+
return Ok(Some((sprint, None)));
212+
}
213+
};
214+
215+
let optionality = optionality.ok_or_else(|| {
216+
Error::UserFacing(format!(
217+
"Failed to parse issue {} - no priority label.{}",
218+
html_url, BAD_LABEL_SUFFIX
200219
))
201220
})?;
202-
// TODO
203-
// let assignment = assignment.ok_or_else(|| Error::UserFacing(format!("Failed to parse issue {} - no submit label", html_url)))?;
204-
let assignment = assignment.unwrap_or(None);
221+
222+
let assignment = match submit_label {
223+
"None" => None,
224+
"PR" => Some(Assignment::ExpectedPullRequest {
225+
title: title.clone(),
226+
optionality,
227+
}),
228+
"Issue" => {
229+
// TODO: Handle these.
230+
None
231+
}
232+
other => {
233+
return Err(Error::UserFacing(format!(
234+
"Failed to parse issue {} - submit label wasn't recognised: {}",
235+
html_url, other
236+
)));
237+
}
238+
};
239+
205240
Ok(Some((sprint, assignment)))
206241
}
207242

243+
const BAD_LABEL_SUFFIX: &str = "\n\nIf this issue was made my a curriculum team member it should be given a sprint label.\nIf this issue was created by a trainee for step submission, it should probably be closed (and they should create the issue in their fork).";
244+
208245
#[derive(Serialize)]
209246
pub struct Course {
210247
pub name: String,
@@ -263,20 +300,34 @@ pub enum Assignment {
263300
},
264301
ExpectedPullRequest {
265302
title: String,
303+
optionality: AssignmentOptionality,
266304
},
267305
}
268306

269307
impl Assignment {
308+
pub fn optionality(&self) -> AssignmentOptionality {
309+
match self {
310+
Assignment::Attendance { .. } => AssignmentOptionality::Mandatory,
311+
Assignment::ExpectedPullRequest { optionality, .. } => optionality.clone(),
312+
}
313+
}
314+
270315
pub fn heading(&self) -> String {
271316
match self {
272317
Assignment::Attendance {
273318
class_dates: _class_dates,
274319
} => "Attendance".to_owned(),
275-
Assignment::ExpectedPullRequest { title } => format!("PR: {title}"),
320+
Assignment::ExpectedPullRequest { title, .. } => format!("PR: {title}"),
276321
}
277322
}
278323
}
279324

325+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
326+
pub enum AssignmentOptionality {
327+
Mandatory,
328+
Stretch,
329+
}
330+
280331
pub struct BatchMembers {
281332
pub name: String,
282333
pub trainees: BTreeMap<GithubLogin, Trainee>,
@@ -363,11 +414,18 @@ impl TraineeWithSubmissions {
363414
Attendance::Absent { .. } => {}
364415
}
365416
}
366-
SubmissionState::Some(Submission::PullRequest { pull_request }) => {
367-
denominator += 10;
417+
SubmissionState::Some(Submission::PullRequest {
418+
pull_request,
419+
optionality,
420+
}) => {
421+
let max = match optionality {
422+
AssignmentOptionality::Mandatory => 10,
423+
AssignmentOptionality::Stretch => 12,
424+
};
425+
denominator += max;
368426
match pull_request.state {
369427
PrState::Complete => {
370-
numerator += 10;
428+
numerator += max;
371429
}
372430
PrState::NeedsReview | PrState::Reviewed => {
373431
numerator += 6;
@@ -381,6 +439,9 @@ impl TraineeWithSubmissions {
381439
Assignment::Attendance { .. } => denominator += 20,
382440
Assignment::ExpectedPullRequest { .. } => denominator += 10,
383441
},
442+
SubmissionState::MissingStretch(_) => {
443+
denominator += 2;
444+
}
384445
SubmissionState::MissingButNotExpected(_) => {}
385446
}
386447
}
@@ -436,23 +497,28 @@ pub struct SprintWithSubmissions {
436497
pub enum SubmissionState {
437498
Some(Submission),
438499
MissingButExpected(Assignment),
500+
MissingStretch(Assignment),
439501
MissingButNotExpected(Assignment),
440502
}
441503

442504
impl SubmissionState {
443-
fn is_missing(&self) -> bool {
505+
fn is_submitted(&self) -> bool {
444506
match self {
445-
Self::Some(_) => false,
446-
Self::MissingButExpected(_) => true,
447-
Self::MissingButNotExpected(_) => true,
507+
Self::Some(_) => true,
508+
Self::MissingButExpected(_) => false,
509+
Self::MissingStretch(_) => false,
510+
Self::MissingButNotExpected(_) => false,
448511
}
449512
}
450513
}
451514

452515
#[derive(Clone, Debug, PartialEq, Eq)]
453516
pub enum Submission {
454517
Attendance(Attendance),
455-
PullRequest { pull_request: Pr },
518+
PullRequest {
519+
pull_request: Pr,
520+
optionality: AssignmentOptionality,
521+
},
456522
}
457523

458524
impl Submission {
@@ -462,14 +528,14 @@ impl Submission {
462528
Self::Attendance(Attendance::OnTime { .. }) => String::from("On time"),
463529
Self::Attendance(Attendance::Late { .. }) => String::from("Late"),
464530
Self::Attendance(Attendance::WrongDay { .. }) => String::from("Wrong day"),
465-
Self::PullRequest { pull_request } => format!("#{}", pull_request.number),
531+
Self::PullRequest { pull_request, .. } => format!("#{}", pull_request.number),
466532
}
467533
}
468534

469535
pub fn link(&self) -> String {
470536
match self {
471537
Self::Attendance(attendance) => attendance.register_url().to_owned(),
472-
Self::PullRequest { pull_request } => pull_request.url.clone(),
538+
Self::PullRequest { pull_request, .. } => pull_request.url.clone(),
473539
}
474540
}
475541
}
@@ -766,20 +832,21 @@ pub fn match_prs_to_assignments(
766832
) -> Result<ModuleWithSubmissions, Error> {
767833
let mut sprints = Vec::with_capacity(module.sprints.len());
768834
for (sprint_index, sprint) in module.sprints.iter().enumerate() {
769-
sprints.push(SprintWithSubmissions {
770-
submissions: vec![
771-
if sprint.is_in_past(region) {
772-
SubmissionState::MissingButExpected(Assignment::Attendance {
773-
class_dates: sprint.dates.clone(),
774-
})
775-
} else {
776-
SubmissionState::MissingButNotExpected(Assignment::Attendance {
777-
class_dates: sprint.dates.clone(),
778-
})
779-
};
780-
sprint.assignment_count()
781-
],
782-
});
835+
let mut submissions = Vec::with_capacity(sprint.assignment_count());
836+
for assignment in sprint.assignments.iter().cloned() {
837+
let submission = if sprint.is_in_past(region) {
838+
match assignment.optionality() {
839+
AssignmentOptionality::Mandatory => {
840+
SubmissionState::MissingButExpected(assignment)
841+
}
842+
AssignmentOptionality::Stretch => SubmissionState::MissingStretch(assignment),
843+
}
844+
} else {
845+
SubmissionState::MissingButNotExpected(assignment)
846+
};
847+
submissions.push(submission);
848+
}
849+
sprints.push(SprintWithSubmissions { submissions });
783850

784851
for (assignment_index, assignment) in sprint.assignments.iter().enumerate() {
785852
if let Assignment::Attendance {
@@ -847,6 +914,7 @@ fn match_pr_to_assignment(
847914
match_count: usize,
848915
sprint_index: usize,
849916
assignment_index: usize,
917+
optionality: AssignmentOptionality,
850918
}
851919

852920
let mut best_match: Option<Match> = None;
@@ -866,6 +934,7 @@ fn match_pr_to_assignment(
866934
match assignment {
867935
Assignment::ExpectedPullRequest {
868936
title: expected_title,
937+
optionality,
869938
} => {
870939
let mut assignment_title_words = make_title_more_matchable(expected_title);
871940
if let Some(claimed_sprint_index) = claimed_sprint_index {
@@ -877,7 +946,7 @@ fn match_pr_to_assignment(
877946
}
878947
}
879948
let match_count = assignment_title_words.intersection(&pr_title_words).count();
880-
if submissions[sprint_index].submissions[assignment_index].is_missing()
949+
if !submissions[sprint_index].submissions[assignment_index].is_submitted()
881950
&& match_count
882951
> best_match
883952
.as_ref()
@@ -888,6 +957,7 @@ fn match_pr_to_assignment(
888957
match_count,
889958
sprint_index,
890959
assignment_index,
960+
optionality: optionality.clone(),
891961
});
892962
}
893963
}
@@ -898,11 +968,15 @@ fn match_pr_to_assignment(
898968
if let Some(Match {
899969
sprint_index,
900970
assignment_index,
971+
optionality,
901972
..
902973
}) = best_match
903974
{
904975
submissions[sprint_index].submissions[assignment_index] =
905-
SubmissionState::Some(Submission::PullRequest { pull_request: pr });
976+
SubmissionState::Some(Submission::PullRequest {
977+
pull_request: pr,
978+
optionality,
979+
});
906980
} else if !pr.is_closed {
907981
unknown_prs.push(pr);
908982
}

src/frontend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ impl TraineeBatchTemplate {
172172
Submission::Attendance(Attendance::WrongDay { .. }) => {
173173
String::from("attendance-wrong-day")
174174
}
175-
Submission::PullRequest { pull_request } => match pull_request.state {
175+
Submission::PullRequest { pull_request, .. } => match pull_request.state {
176176
PrState::NeedsReview => "pr-needs-review".to_owned(),
177177
PrState::Reviewed => "pr-reviewed".to_owned(),
178178
PrState::Complete => "pr-complete".to_owned(),

templates/trainee-batch.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
--green: #adf7c7;
77
--orange: #f8bca3;
88
--red: #ffaaaa;
9+
--yellow: #e6f4ae;
910
}
1011
th, td {
1112
border: 1px black solid;
@@ -18,14 +19,17 @@
1819
background-color: var(--green);
1920
}
2021
td.attendance-late {
21-
background-color: #e6f4ae;
22+
background-color: var(--yellow);
2223
}
2324
td.attendance-wrong-day {
2425
background-color: grey;
2526
}
2627
td.pr-missing {
2728
background-color: var(--red);
2829
}
30+
td.pr-missing-stretch {
31+
background-color: var(--yellow);
32+
}
2933
td.pr-complete {
3034
background-color: var(--green);
3135
}
@@ -38,7 +42,6 @@
3842
td.pr-unknown {
3943
background-color: grey;
4044
}
41-
4245
.trainee-on-track {
4346
background-color: var(--green);
4447
}
@@ -124,6 +127,8 @@ <h1>{{ course.name }} - {{ batch.name }}</h1>
124127
<td class="{{ css_classes_for_submission(submission) }}"><a href="{{ submission.link() }}">{{ submission.display_text() }}</a></td>
125128
{% when crate::course::SubmissionState::MissingButExpected(_) %}
126129
<td class="pr-missing"></td>
130+
{% when crate::course::SubmissionState::MissingStretch(_) %}
131+
<td class="pr-missing-stretch"></td>
127132
{% when crate::course::SubmissionState::MissingButNotExpected(_) %}
128133
<td></td>
129134
{% endmatch %}

0 commit comments

Comments
 (0)