Skip to content

Commit 13b7353

Browse files
committed
initial addition of filtering mechanism
1 parent 9d63827 commit 13b7353

File tree

3 files changed

+89790
-6
lines changed

3 files changed

+89790
-6
lines changed

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

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
use catalyst_toolbox::ideascale::{
2-
build_challenges, build_fund, build_proposals, fetch_all, CustomFieldTags,
3-
Error as IdeascaleError, Scores, Sponsors,
1+
use catalyst_toolbox::{
2+
community_advisors::models::{ReviewRanking, VeteranRankingRow},
3+
ideascale::{
4+
build_challenges, build_fund, build_proposals, fetch_all, CustomFieldTags,
5+
Error as IdeascaleError, Scores, Sponsors,
6+
},
7+
utils::csv::{dump_data_to_csv, load_data_from_csv},
48
};
9+
use core::cmp::max;
10+
use itertools::Itertools;
511
use jcli_lib::utils::io as io_utils;
612
use jormungandr_lib::interfaces::VotePrivacy;
7-
use std::collections::HashSet;
13+
use std::{collections::HashSet, ffi::OsStr};
814

915
use structopt::StructOpt;
1016

@@ -30,6 +36,7 @@ pub enum Error {
3036
#[derive(Debug, StructOpt)]
3137
pub enum Ideascale {
3238
Import(Import),
39+
Filter(Filter),
3340
}
3441

3542
// We need this type because structopt uses Vec<String> as a special type, so it is not compatible
@@ -88,10 +95,21 @@ pub struct Import {
8895
stages_filters: Filters,
8996
}
9097

98+
#[derive(Debug, StructOpt)]
99+
#[structopt(rename_all = "kebab")]
100+
pub struct Filter {
101+
#[structopt(long)]
102+
input: PathBuf,
103+
104+
#[structopt(long)]
105+
output: Option<PathBuf>,
106+
}
107+
91108
impl Ideascale {
92109
pub fn exec(&self) -> Result<(), Error> {
93110
match self {
94111
Ideascale::Import(import) => import.exec(),
112+
Ideascale::Filter(filter) => filter.exec(),
95113
}
96114
}
97115
}
@@ -182,6 +200,70 @@ impl Import {
182200
}
183201
}
184202

203+
impl Filter {
204+
fn output_file(input: &Path, output: Option<&Path>) -> PathBuf {
205+
if let Some(output) = output {
206+
output.to_path_buf()
207+
} else {
208+
let name = input.file_name().and_then(OsStr::to_str).unwrap_or("");
209+
let name = format!("{name}.output");
210+
let temp = input.with_file_name(name);
211+
println!("no output specified, writing to {}", temp.to_string_lossy());
212+
temp
213+
}
214+
}
215+
216+
fn filter_rows(rows: &[VeteranRankingRow]) -> Vec<VeteranRankingRow> {
217+
let groups = rows
218+
.iter()
219+
.group_by(|row| (&row.assessor, &row.proposal_id));
220+
groups
221+
.into_iter()
222+
.flat_map(|(_, group)| {
223+
let group = group.collect_vec();
224+
let excellent = group
225+
.iter()
226+
.filter(|row| row.score() == ReviewRanking::Excellent)
227+
.count();
228+
let good = group
229+
.iter()
230+
.filter(|row| row.score() == ReviewRanking::Good)
231+
.count();
232+
let filtered = group
233+
.iter()
234+
.filter(|row| row.score() == ReviewRanking::FilteredOut)
235+
.count();
236+
237+
let max_count = max(excellent, max(good, filtered));
238+
239+
let include_excellent = excellent == max_count;
240+
let include_good = good == max_count;
241+
let include_filtered = filtered == max_count;
242+
243+
group.into_iter().filter(move |row| match row.score() {
244+
ReviewRanking::Excellent => include_excellent,
245+
ReviewRanking::Good => include_good,
246+
ReviewRanking::FilteredOut => include_filtered,
247+
ReviewRanking::NA => true, // if unknown, ignore
248+
})
249+
})
250+
.cloned()
251+
.collect()
252+
}
253+
254+
fn exec(&self) -> Result<(), Error> {
255+
let Self { input, output } = self;
256+
let output = Self::output_file(input, output.as_deref());
257+
258+
let rows = load_data_from_csv::<_, b','>(input)?;
259+
let rows = Self::filter_rows(&rows);
260+
261+
dump_data_to_csv(&rows, &output)?;
262+
263+
Ok(())
264+
}
265+
}
266+
185267
fn dump_content_to_file(content: impl Serialize, file_path: &Path) -> Result<(), Error> {
186268
let writer = jcli_lib::utils::io::open_file_write(&Some(file_path))?;
187269
serde_json::to_writer_pretty(writer, &content).map_err(Error::Serde)
@@ -238,3 +320,36 @@ fn read_sponsors_file(path: &Option<PathBuf>) -> Result<Sponsors, Error> {
238320
}
239321
Ok(sponsors)
240322
}
323+
324+
#[cfg(test)]
325+
mod tests {
326+
use super::*;
327+
328+
#[test]
329+
fn correctly_formats_output_file_for_filter() {
330+
let input = PathBuf::from("/foo/bar/file.txt");
331+
let output = PathBuf::from("/baz/qux/output.txt");
332+
333+
let result = Filter::output_file(&input, Some(&output));
334+
assert_eq!(result, output);
335+
336+
let result = Filter::output_file(&input, None);
337+
assert_eq!(result, PathBuf::from("/foo/bar/file.txt.output"));
338+
}
339+
340+
#[test]
341+
fn filters_rows_correctly() {
342+
use ReviewRanking::*;
343+
344+
let pid = String::from("pid");
345+
let assessor = String::from("assessor");
346+
let first = VeteranRankingRow::new(pid.clone(), assessor.clone(), "1".into(), Excellent);
347+
let second = VeteranRankingRow::new(pid.clone(), assessor.clone(), "2".into(), Excellent);
348+
let third = VeteranRankingRow::new(pid.clone(), assessor.clone(), "3".into(), Good);
349+
350+
let rows = vec![first.clone(), second.clone(), third];
351+
let expected_rows = vec![first, second];
352+
353+
assert_eq!(Filter::filter_rows(&rows), expected_rows);
354+
}
355+
}

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

Lines changed: 22 additions & 2 deletions
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
}

0 commit comments

Comments
 (0)