Skip to content

Commit d79958f

Browse files
authored
Merge pull request #132 from input-output-hk/filter-vca
Filter dissenting vCAs
2 parents ea16440 + 94760ae commit d79958f

File tree

3 files changed

+141
-6
lines changed

3 files changed

+141
-6
lines changed

catalyst-toolbox/src/bin/cli/ideascale/mod.rs

+118-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use catalyst_toolbox::ideascale::{
2-
build_challenges, build_fund, build_proposals, fetch_all, CustomFieldTags, Scores, Sponsors,
1+
use catalyst_toolbox::{
2+
community_advisors::models::{ReviewRanking, VeteranRankingRow},
3+
ideascale::{
4+
build_challenges, build_fund, build_proposals, fetch_all, CustomFieldTags, Scores, Sponsors,
5+
},
6+
utils::csv::{dump_data_to_csv, load_data_from_csv},
37
};
48
use color_eyre::Report;
9+
use itertools::Itertools;
510
use jcli_lib::utils::io as io_utils;
611
use jormungandr_lib::interfaces::VotePrivacy;
7-
use std::collections::HashSet;
12+
use std::{collections::HashSet, ffi::OsStr};
813

914
use structopt::StructOpt;
1015

@@ -17,6 +22,7 @@ use std::path::{Path, PathBuf};
1722
#[derive(Debug, StructOpt)]
1823
pub enum Ideascale {
1924
Import(Import),
25+
Filter(Filter),
2026
}
2127

2228
// We need this type because structopt uses Vec<String> as a special type, so it is not compatible
@@ -75,10 +81,21 @@ pub struct Import {
7581
stages_filters: Filters,
7682
}
7783

84+
#[derive(Debug, StructOpt)]
85+
#[structopt(rename_all = "kebab")]
86+
pub struct Filter {
87+
#[structopt(long)]
88+
input: PathBuf,
89+
90+
#[structopt(long)]
91+
output: Option<PathBuf>,
92+
}
93+
7894
impl Ideascale {
7995
pub fn exec(&self) -> Result<(), Report> {
8096
match self {
8197
Ideascale::Import(import) => import.exec(),
98+
Ideascale::Filter(filter) => filter.exec(),
8299
}
83100
}
84101
}
@@ -166,6 +183,71 @@ impl Import {
166183
}
167184
}
168185

186+
impl Filter {
187+
fn output_file(input: &Path, output: Option<&Path>) -> PathBuf {
188+
if let Some(output) = output {
189+
output.to_path_buf()
190+
} else {
191+
let name = input.file_name().and_then(OsStr::to_str).unwrap_or("");
192+
let name = format!("{name}.output");
193+
let temp = input.with_file_name(name);
194+
println!("no output specified, writing to {}", temp.to_string_lossy());
195+
temp
196+
}
197+
}
198+
199+
fn filter_rows(rows: &[VeteranRankingRow]) -> Vec<VeteranRankingRow> {
200+
let groups = rows
201+
.iter()
202+
.group_by(|row| (&row.assessor, &row.proposal_id));
203+
groups
204+
.into_iter()
205+
.flat_map(|(_, group)| {
206+
let group = group.collect_vec();
207+
let excellent = group
208+
.iter()
209+
.filter(|row| row.score() == ReviewRanking::Excellent)
210+
.count();
211+
let good = group
212+
.iter()
213+
.filter(|row| row.score() == ReviewRanking::Good)
214+
.count();
215+
let filtered = group
216+
.iter()
217+
.filter(|row| row.score() == ReviewRanking::FilteredOut)
218+
.count();
219+
220+
use std::cmp::max;
221+
let max_count = max(excellent, max(good, filtered));
222+
223+
let include_excellent = excellent == max_count;
224+
let include_good = good == max_count;
225+
let include_filtered = filtered == max_count;
226+
227+
group.into_iter().filter(move |row| match row.score() {
228+
ReviewRanking::Excellent => include_excellent,
229+
ReviewRanking::Good => include_good,
230+
ReviewRanking::FilteredOut => include_filtered,
231+
ReviewRanking::NA => true, // if unknown, ignore
232+
})
233+
})
234+
.cloned()
235+
.collect()
236+
}
237+
238+
fn exec(&self) -> Result<(), Report> {
239+
let Self { input, output } = self;
240+
let output = Self::output_file(input, output.as_deref());
241+
242+
let rows = load_data_from_csv::<_, b','>(input)?;
243+
let rows = Self::filter_rows(&rows);
244+
245+
dump_data_to_csv(&rows, &output)?;
246+
247+
Ok(())
248+
}
249+
}
250+
169251
fn dump_content_to_file(content: impl Serialize, file_path: &Path) -> Result<(), Report> {
170252
let writer = jcli_lib::utils::io::open_file_write(&Some(file_path))?;
171253
serde_json::to_writer_pretty(writer, &content)?;
@@ -223,3 +305,36 @@ fn read_sponsors_file(path: &Option<PathBuf>) -> Result<Sponsors, Report> {
223305
}
224306
Ok(sponsors)
225307
}
308+
309+
#[cfg(test)]
310+
mod tests {
311+
use super::*;
312+
313+
#[test]
314+
fn correctly_formats_output_file_for_filter() {
315+
let input = PathBuf::from("/foo/bar/file.txt");
316+
let output = PathBuf::from("/baz/qux/output.txt");
317+
318+
let result = Filter::output_file(&input, Some(&output));
319+
assert_eq!(result, output);
320+
321+
let result = Filter::output_file(&input, None);
322+
assert_eq!(result, PathBuf::from("/foo/bar/file.txt.output"));
323+
}
324+
325+
#[test]
326+
fn filters_rows_correctly() {
327+
use ReviewRanking::*;
328+
329+
let pid = String::from("pid");
330+
let assessor = String::from("assessor");
331+
let first = VeteranRankingRow::new(pid.clone(), assessor.clone(), "1".into(), Excellent);
332+
let second = VeteranRankingRow::new(pid.clone(), assessor.clone(), "2".into(), Excellent);
333+
let third = VeteranRankingRow::new(pid.clone(), assessor.clone(), "3".into(), Good);
334+
335+
let rows = vec![first.clone(), second.clone(), third];
336+
let expected_rows = vec![first, second];
337+
338+
assert_eq!(Filter::filter_rows(&rows), expected_rows);
339+
}
340+
}

catalyst-toolbox/src/community_advisors/models/de.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::utils::serde::deserialize_truthy_falsy;
2-
use serde::Deserialize;
2+
use serde::{Deserialize, Serialize};
33
use vit_servicing_station_lib::db::models::community_advisors_reviews::ReviewRanking as VitReviewRanking;
44

55
/// (Proposal Id, Assessor Id), an assessor cannot assess the same proposal more than once
@@ -37,7 +37,7 @@ pub struct AdvisorReviewRow {
3737
filtered_out: bool,
3838
}
3939

40-
#[derive(Deserialize)]
40+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
4141
pub struct VeteranRankingRow {
4242
pub proposal_id: String,
4343
#[serde(alias = "Assessor")]
@@ -82,6 +82,26 @@ impl ReviewRanking {
8282
}
8383

8484
impl VeteranRankingRow {
85+
pub fn new(
86+
proposal_id: String,
87+
assessor: String,
88+
vca: VeteranAdvisorId,
89+
ranking: ReviewRanking,
90+
) -> Self {
91+
let excellent = ranking == ReviewRanking::Excellent;
92+
let good = ranking == ReviewRanking::Good;
93+
let filtered_out = ranking == ReviewRanking::FilteredOut;
94+
95+
Self {
96+
proposal_id,
97+
assessor,
98+
vca,
99+
excellent,
100+
good,
101+
filtered_out,
102+
}
103+
}
104+
85105
pub fn score(&self) -> ReviewRanking {
86106
ranking_mux(self.excellent, self.good, self.filtered_out)
87107
}

catalyst-toolbox/src/utils/csv.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn load_data_from_csv<T: DeserializeOwned, const DELIMITER: u8>(
1313
}
1414

1515
pub fn dump_data_to_csv<'a, T: 'a + Serialize>(
16-
data: impl Iterator<Item = &'a T>,
16+
data: impl IntoIterator<Item = &'a T>,
1717
file_path: &Path,
1818
) -> Result<(), csv::Error> {
1919
let mut writer = csv::WriterBuilder::new()

0 commit comments

Comments
 (0)