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 ;
511use jcli_lib:: utils:: io as io_utils;
612use jormungandr_lib:: interfaces:: VotePrivacy ;
7- use std:: collections:: HashSet ;
13+ use std:: { collections:: HashSet , ffi :: OsStr } ;
814
915use structopt:: StructOpt ;
1016
@@ -30,6 +36,7 @@ pub enum Error {
3036#[ derive( Debug , StructOpt ) ]
3137pub 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+
91108impl 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+
185267fn 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+ }
0 commit comments