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} ;
48use color_eyre:: Report ;
9+ use itertools:: Itertools ;
510use jcli_lib:: utils:: io as io_utils;
611use jormungandr_lib:: interfaces:: VotePrivacy ;
7- use std:: collections:: HashSet ;
12+ use std:: { collections:: HashSet , ffi :: OsStr } ;
813
914use structopt:: StructOpt ;
1015
@@ -17,6 +22,7 @@ use std::path::{Path, PathBuf};
1722#[ derive( Debug , StructOpt ) ]
1823pub 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+
7894impl 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+
169251fn 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+ }
0 commit comments