diff --git a/Changelog.md b/Changelog.md index 76a586c9785d44876a9da39ea70b5ac89ca34ea2..35e91834e084d7c695484a8753968f05021dd339 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.8] - 2023-03-21 +- Add survey-publisher-v1 +- Add survey-publisher-2022 +- Add funding sources API +- Add initial funding sources frontend work + ## [0.7] - 2023-02-22 - Fix API call URL http -> https redirect bug diff --git a/compendium_v2/app.py b/compendium_v2/app.py index acd1fab0d684e63453a3596a204430314cfefea1..33cd019af8ffc615457ed6d02f81b6dbf108d0d6 100644 --- a/compendium_v2/app.py +++ b/compendium_v2/app.py @@ -3,7 +3,6 @@ default app creation """ import compendium_v2 from compendium_v2 import environment - environment.setup_logging() app = compendium_v2.create_app() diff --git a/compendium_v2/background_task/__init__.py b/compendium_v2/background_task/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/compendium_v2/background_task/csv/BudgetCsvFile.csv b/compendium_v2/background_task/csv/BudgetCsvFile.csv new file mode 100644 index 0000000000000000000000000000000000000000..09ac009cfd38b042651034b8fa4b53985e10fb51 --- /dev/null +++ b/compendium_v2/background_task/csv/BudgetCsvFile.csv @@ -0,0 +1,180 @@ +ACOnet,6.4,2021 +ACOnet,6.4,2020 +ACOnet,6.1,2019 +ACOnet,6.1,2018 +ACOnet,5.7,2017 +AMRES,3.1,2021 +AMRES,2.35,2020 +AMRES,2.47,2019 +AMRES,2.02,2018 +AMRES,1.77,2017 +ANA,0.8,2021 +ANA,0.8,2020 +ANA,0.8,2019 +ANA,0.8,2018 +ARNES,9.8,2021 +ARNES,9.0,2020 +ARNES,8.0,2019 +ARNES,7.5,2018 +ARNES,6.0,2017 +ASNET,0.5,2021 +ASNET,0.48,2020 +ASNET,0.48,2019 +ASNET,0.25,2018 +ASNET,0.15,2017 +AzScienceNet,1.2,2021 +AzScienceNet,1.2,2020 +AzScienceNet,1.2,2019 +AzScienceNet,2.0,2018 +AzScienceNet,0.0,2017 +BASNET,0.63,2021 +BASNET,0.94,2020 +BASNET,0.92,2019 +BASNET,0.72,2018 +BASNET,0.82,2017 +BELNET,17.3,2021 +BELNET,13.85,2020 +BELNET,14.48,2019 +BELNET,15.52,2018 +BREN,0.03,2021 +CARNet,86.08,2021 +CARNet,30.31,2020 +CARNet,36.45,2019 +CARNet,20.1,2018 +CARNet,34.73,2017 +CESNET,19.5,2021 +CESNET,18.86,2020 +CESNET,18.97,2019 +CESNET,17.88,2018 +CESNET,14.7,2017 +CYNET,0.97,2021 +CYNET,0.92,2020 +CYNET,0.92,2019 +CYNET,3.6,2018 +CYNET,0.77,2017 +DeIC,9.92,2021 +DeIC,7.95,2020 +DeIC,7.1,2019 +DeIC,7.41,2018 +DFN,51.78,2021 +DFN,50.37,2020 +DFN,42.34,2019 +DFN,42.0,2018 +DFN,45.0,2017 +EENet,1.29,2021 +EENet,1.29,2020 +EENet,4.77,2019 +EENet,2.34,2018 +EENet,2.34,2017 +FCCN,24.82,2021 +FCCN,20.11,2020 +FCCN,16.25,2019 +FCCN,14.25,2018 +FCCN,8.07,2017 +Funet,8.3,2021 +Funet,8.3,2020 +Funet,7.9,2019 +Funet,7.5,2018 +Funet,7.5,2017 +GARR,22.0,2021 +GARR,23.0,2020 +GARR,22.33,2019 +GARR,22.15,2018 +GARR,21.97,2017 +GRENA,0.4,2021 +GRENA,0.4,2020 +GRENA,0.4,2019 +GRENA,0.3,2018 +GRENA,0.3,2017 +GRNET S.A.,7.4,2021 +GRNET S.A.,7.0,2020 +GRNET S.A.,6.9,2019 +GRNET S.A.,6.9,2018 +GRNET S.A.,6.5,2017 +HEAnet,25.2,2021 +HEAnet,24.89,2020 +HEAnet,25.59,2019 +HEAnet,25.06,2018 +HEAnet,29.6,2017 +IUCC,4.28,2021 +IUCC,4.3,2020 +IUCC,3.48,2019 +IUCC,3.72,2018 +IUCC,3.9,2017 +Jisc,69.61,2021 +Jisc,63.41,2020 +Jisc,65.23,2019 +KIFU (NIIF),34.0,2021 +KIFU (NIIF),34.0,2020 +KIFU (NIIF),48.0,2019 +KIFU (NIIF),12.0,2018 +KIFU (NIIF),12.0,2017 +LAT,1.24,2019 +LAT,1.03,2018 +LAT,0.0,2017 +LITNET,2.65,2021 +LITNET,2.33,2020 +LITNET,2.28,2019 +LITNET,2.28,2018 +LITNET,2.06,2017 +MARNET,0.63,2021 +MARNET,1.14,2020 +MARNET,0.95,2019 +MARNET,0.33,2018 +MREN,0.07,2021 +MREN,0.07,2020 +MREN,0.07,2019 +MREN,0.07,2018 +PIONIER,0.0,2020 +RedIRIS,23.0,2021 +RedIRIS,8.0,2020 +RedIRIS,8.0,2019 +RedIRIS,8.0,2018 +RedIRIS,8.0,2017 +RENAM,0.45,2021 +RENAM,0.32,2020 +RENAM,0.38,2019 +RENAM,0.34,2018 +RENAM,0.38,2017 +RENATER,39.0,2021 +RENATER,31.1,2020 +RENATER,32.2,2019 +RENATER,33.9,2018 +RENATER,29.1,2017 +RESTENA,4.54,2021 +RESTENA,4.75,2020 +RESTENA,4.03,2019 +RESTENA,3.48,2018 +RESTENA,2.06,2017 +RhNET,0.0,2019 +RoEduNet,2.0,2020 +RoEduNet,2.0,2019 +RoEduNet,2.0,2018 +RoEduNet,2.0,2017 +SANET,1.98,2021 +SANET,2.18,2020 +SANET,1.98,2019 +SANET,1.98,2018 +SANET,1.98,2017 +SUNET,28.0,2021 +SUNET,26.0,2020 +SUNET,24.0,2019 +SUNET,19.0,2018 +SUNET,0.0,2017 +SURF,54.97,2020 +SURF,52.86,2019 +SURF,40.5,2018 +SURF,40.2,2017 +SWITCH,30.56,2021 +SWITCH,27.74,2020 +SWITCH,13.46,2019 +SWITCH,14.0,2018 +SWITCH,14.2,2017 +ULAKBIM,17.0,2021 +ULAKBIM,17.2,2020 +ULAKBIM,17.0,2019 +ULAKBIM,21.0,2018 +ULAKBIM,18.5,2017 +UNINETT,15.0,2019 +UNINETT,20.0,2018 +UNINETT,30.0,2017 diff --git a/compendium_v2/background_task/csv/FundingSourceCsvFile.csv b/compendium_v2/background_task/csv/FundingSourceCsvFile.csv new file mode 100644 index 0000000000000000000000000000000000000000..2a7b4a72b72c9553c8d29e2e74dedc595d345825 --- /dev/null +++ b/compendium_v2/background_task/csv/FundingSourceCsvFile.csv @@ -0,0 +1,198 @@ +ACOnet,2020,100.0,0.0,0.0,0.0,0.0 +AMRES,2020,0.0,0.0,100.0,0.0,0.0 +ANA,2020,13.0,1.0,1.0,1.0,0.0 +ARNES,2020,0.0,5.0,84.0,11.0,0.0 +ASNET-AM,2020,10.0,48.0,40.0,0.0,1.0 +AzScienceNet,2020,60.0,40.0,60.0,0.0,0.0 +BASNET,2020,26.0,64.0,10.0,0.0,0.0 +Belnet,2020,39.0,2.0,52.0,7.0,0.0 +BREN,2020,25.0,0.0,0.0,0.0,0.0 +CARNET,2020,0.0,59.32,37.64,3.04,0.0 +CESNET,2020,21.0,3.0,73.0,0.0,2.0 +CYNET,2020,63.0,25.0,12.0,0.0,0.0 +DeIC,2020,95.0,1.0,0.0,4.0,0.0 +DFN,2020,94.0,3.0,1.5,0.0,1.5 +EENet,2020,0.0,71.0,26.0,0.0,0.0 +FCCN,2020,3.07,0.21,95.85,0.0,0.87 +Funet,2020,60.0,0.0,40.0,0.0,0.0 +GARR,2020,88.92,3.69,7.34,0.0,0.04 +GRENA,2020,40.0,50.0,0.0,5.0,5.0 +GRNET S.A.,2020,0.0,20.0,75.0,0.0,5.0 +HEAnet,2020,16.0,1.0,74.0,9.0,0.0 +IUCC,2020,91.0,9.0,0.0,0.0,0.0 +Jisc,2020,20.069,1.562,48.554,29.814,0.0 +KIFU (NIIF),2020,4.0,2.0,94.0,0.0,0.0 +LITNET,2020,10.0,33.0,57.0,0.0,0.0 +MARnet,2020,0.0,20.0,56.0,24.0,0.0 +MREN,2020,0.0,20.0,70.0,0.0,5.0 +PIONIER,2020,0.0,0.0,0.0,0.0,0.0 +RedIRIS,2020,0.0,7.0,92.0,0.0,1.0 +RENAM,2020,24.0,74.0,2.0,0.0,0.0 +RENATER,2020,11.0,4.0,85.0,0.0,0.0 +RESTENA,2020,6.0,3.0,34.0,37.0,20.0 +RhNET,2020,0.0,0.0,0.0,0.0,0.0 +RoEduNet,2020,0.0,0.0,100.0,0.0,0.0 +SANET,2020,7.0,0.0,93.0,0.0,0.0 +LAT,2020,0.0,0.0,0.0,0.0,0.0 +SUNET,2020,70.0,0.0,20.0,0.0,10.0 +SURFnet,2020,60.0,2.0,38.0,0.0,0.0 +SWITCH,2020,55.5,0.9,1.1,42.5,0.0 +ULAKBIM,2020,0.0,0.3,99.5,0.0,0.2 +UNINETT,2020,0.0,0.0,0.0,0.0,0.0 +UoM,2020,0.0,0.0,0.0,0.0,0.0 +ACOnet,2019,100.0,0.0,0.0,0.0,0.0 +AMRES,2019,0.0,0.0,100.0,0.0,0.0 +ANA,2019,13.0,1.0,1.0,1.0,0.0 +ARNES,2019,0.0,15.0,72.0,13.0,0.0 +ASNET-AM,2019,0.0,0.0,0.0,0.0,0.0 +AzScienceNet,2019,58.0,40.0,60.0,0.0,0.0 +BASNET,2019,27.0,64.0,9.0,0.0,0.0 +Belnet,2019,38.46,2.4,52.16,6.98,0.0 +BREN,2019,25.0,0.0,0.0,0.0,0.0 +CARNET,2019,0.0,59.11,37.89,3.0,0.0 +CESNET,2019,23.0,4.0,72.0,1.0,0.0 +CYNET,2019,54.6,29.6,15.8,0.0,0.0 +DeIC,2019,0.0,0.0,0.0,0.0,0.0 +DFN,2019,98.0,2.0,0.0,0.0,0.0 +EENet,2019,3.0,71.0,26.0,0.0,0.0 +FCCN,2019,2.0,0.22,96.4,0.0,1.29 +Funet,2019,60.0,0.0,40.0,0.0,0.0 +GARR,2019,87.66,4.73,7.57,0.0,0.04 +GRENA,2019,40.0,50.0,0.0,5.0,5.0 +GRNET S.A.,2019,0.0,20.0,75.0,0.0,5.0 +HEAnet,2019,14.0,1.0,84.0,1.0,0.0 +IUCC,2019,86.0,14.0,0.0,0.0,0.0 +Jisc,2019,23.0,1.0,70.0,6.0,0.0 +KIFU (NIIF),2019,3.0,2.0,95.0,0.0,0.0 +LITNET,2019,0.0,0.0,0.0,0.0,0.0 +MARnet,2019,14.0,17.0,69.0,0.0,0.0 +MREN,2019,0.0,14.0,66.0,20.0,0.0 +PIONIER,2019,0.0,0.0,0.0,0.0,0.0 +RedIRIS,2019,0.0,7.0,92.0,0.0,1.0 +RENAM,2019,39.0,60.0,1.0,0.0,0.0 +RENATER,2019,30.0,4.0,64.0,1.0,1.0 +RESTENA,2019,5.0,5.0,37.0,38.0,15.0 +RhNET,2019,0.0,0.0,0.0,0.0,0.0 +RoEduNet,2019,0.0,0.0,100.0,0.0,0.0 +SANET,2019,7.0,0.0,93.0,0.0,0.0 +LAT,2019,0.0,0.0,0.0,0.0,0.0 +SUNET,2019,70.0,0.0,25.0,0.0,5.0 +SURFnet,2019,58.0,2.0,40.0,0.0,0.0 +SWITCH,2019,48.0,1.0,1.0,50.0,0.0 +ULAKBIM,2019,0.0,0.0,99.5,0.0,0.0 +UNINETT,2019,0.0,0.0,0.0,0.0,0.0 +UoM,2019,0.0,0.0,0.0,0.0,0.0 +ACOnet,2018,100.0,0.0,0.0,0.0,0.0 +AMRES,2018,0.0,0.0,100.0,0.0,0.0 +ANA,2018,13.0,1.0,1.0,1.0,0.0 +ARNES,2018,0.0,4.0,84.0,12.0,0.0 +ASNET-AM,2018,20.0,40.0,40.0,0.0,0.0 +AzScienceNet,2018,0.0,95.0,5.0,0.0,0.0 +BASNET,2018,34.0,58.0,8.0,0.0,0.0 +Belnet,2018,20.83,1.99,71.41,5.77,0.0 +BREN,2018,0.0,0.0,0.0,0.0,0.0 +CARNET,2018,0.0,34.6,62.21,3.19,0.0 +CESNET,2018,22.0,7.0,70.0,1.0,0.0 +CYNET,2018,13.0,20.0,63.3,0.0,0.0 +DeIC,2018,99.0,1.0,0.0,0.0,0.0 +DFN,2018,96.0,2.0,0.0,0.0,2.0 +EENet,2018,5.0,42.0,50.0,0.0,0.0 +FCCN,2018,2.0,0.54,95.74,0.0,1.72 +Funet,2018,60.0,0.0,40.0,0.0,0.0 +GARR,2018,87.55,4.77,7.63,0.0,0.05 +GRENA,2018,40.0,45.0,0.0,5.0,10.0 +GRNET S.A.,2018,0.0,35.0,60.0,0.0,5.0 +HEAnet,2018,10.0,1.0,86.0,1.0,0.0 +IUCC,2018,86.0,14.0,0.0,0.0,0.0 +Jisc,2018,0.0,0.0,0.0,0.0,0.0 +KIFU (NIIF),2018,10.0,5.0,85.0,0.0,0.0 +LITNET,2018,14.0,20.0,66.0,0.0,0.0 +MARnet,2018,0.0,25.0,42.0,33.0,0.0 +MREN,2018,0.0,20.0,70.0,0.0,10.0 +PIONIER,2018,0.0,0.0,0.0,0.0,0.0 +RedIRIS,2018,0.0,7.0,92.0,0.0,1.0 +RENAM,2018,52.0,46.0,2.0,0.0,0.0 +RENATER,2018,30.0,4.0,65.0,0.0,1.0 +RESTENA,2018,6.0,2.0,40.0,37.0,15.0 +RhNET,2018,0.0,0.0,0.0,0.0,0.0 +RoEduNet,2018,0.0,0.0,100.0,0.0,0.0 +SANET,2018,7.0,0.0,93.0,0.0,0.0 +LAT,2018,0.0,0.0,0.0,0.0,0.0 +SUNET,2018,70.0,0.0,25.0,0.0,5.0 +SURFnet,2018,66.0,3.0,31.0,0.0,0.0 +SWITCH,2018,52.0,1.0,3.0,44.0,0.0 +ULAKBIM,2018,0.0,0.5,99.4,0.0,0.1 +UNINETT,2018,75.0,0.0,25.0,0.0,0.0 +UoM,2018,0.0,0.0,0.0,0.0,0.0 +ACOnet,2017,100.0,0.0,0.0,0.0,0.0 +AMRES,2017,0.0,0.0,13.5,86.5,0.0 +ARNES,2017,0.0,12.0,3.0,73.0,12.0 +ASNET-AM,2017,15.0,0.0,1.0,30.0,40.0 +AzScienceNet,2017,0.0,0.0,0.0,2.0,0.0 +BASNET,2017,56.0,0.0,3.0,17.0,24.0 +CARNet,2017,0.0,1.94,1.99,40.4,55.68 +CESNET,2017,23.0,0.0,3.0,69.0,3.0 +CYNET,2017,64.1,3.8,30.8,0.0,0.0 +DFN,2017,89.0,0.0,2.0,0.0,0.0 +EENet,2017,5.0,0.0,6.0,50.0,42.0 +FCCN,2017,4.18,0.0,4.54,90.64,0.64 +Funet,2017,60.0,0.0,0.0,40.0,0.0 +GARR,2017,88.0,0.0,3.23,7.56,1.72 +GRENA,2017,35.0,5.0,0.0,0.0,50.0 +GRNET S.A.,2017,0.0,0.0,0.0,55.0,50.0 +HEAnet,2017,10.0,1.0,2.0,86.0,1.0 +IUCC,2017,85.0,0.0,10.0,0.0,5.0 +Jisc,2017,0.0,0.0,0.0,0.0,0.0 +KIFU (NIIF),2017,20.0,0.0,5.0,70.0,5.0 +LITNET,2017,0.0,0.0,5.0,72.0,22.0 +MARnet,2017,0.0,0.0,0.0,0.0,0.0 +MREN,2017,0.0,0.0,0.0,1.0,0.0 +PIONIER,2017,0.0,0.0,0.0,0.0,0.0 +RedIRIS,2017,0.0,0.0,6.0,92.0,1.0 +RENAM,2017,1.0,0.0,15.0,3.0,24.0 +RENATER,2017,34.0,1.0,4.0,58.0,2.0 +RESTENA,2017,10.0,20.0,7.0,63.0,0.0 +RoEduNet,2017,0.0,0.0,1.0,100.0,0.0 +SANET,2017,0.0,0.0,0.0,0.0,0.0 +SigmaNet,2017,0.0,0.0,0.0,0.0,0.0 +SUNET,2017,75.0,0.0,0.0,25.0,0.0 +SURFnet,2017,70.0,0.0,3.0,27.0,0.0 +SWITCH,2017,46.0,47.0,3.0,4.0,0.0 +ULAKBIM,2017,0.0,0.0,2.0,0.0,0.0 +UNINETT,2017,0.0,0.0,0.0,0.0,0.0 +ACOnet,2016,0.0,0.0,0.0,0.0,0.0 +AMRES,2016,0.0,13.5,86.5,0.0,0.0 +ARNES,2016,16.0,0.0,84.0,2.0,1.0 +ASNET-AM,2016,0.0,0.0,0.0,0.0,0.0 +AzScienceNet,2016,0.0,0.0,0.0,0.0,0.0 +BASNET,2016,0.0,3.0,17.0,24.0,0.0 +CARNet,2016,2.05,2.08,0.0,63.34,0.0 +CESNET,2016,0.0,3.0,69.0,3.0,2.0 +CYNET,2016,0.0,20.0,0.0,20.0,1.0 +DFN,2016,0.0,1.0,0.0,0.0,17.0 +EENet,2016,0.0,6.0,50.0,42.0,0.0 +FCCN,2016,0.0,4.6,91.5,1.15,0.39 +Funet,2016,0.0,0.0,40.0,0.0,0.0 +GARR,2016,0.0,3.24,9.64,0.43,0.09 +GRENA,2016,5.0,0.0,0.0,50.0,10.0 +GRNET S.A.,2016,0.0,0.0,55.0,50.0,5.0 +HEAnet,2016,2.0,2.0,86.0,1.0,0.0 +IUCC,2016,0.0,14.0,0.0,0.0,17.0 +Jisc,2016,0.0,0.0,0.0,0.0,0.0 +KIFU (NIIF),2016,0.0,3.0,2.0,13.0,0.0 +LITNET,2016,0.0,5.0,88.0,8.0,0.0 +MARnet,2016,0.0,0.0,0.0,0.0,0.0 +MREN,2016,0.0,0.0,1.0,0.0,0.0 +PIONIER,2016,0.0,0.0,0.0,0.0,0.0 +RedIRIS,2016,0.0,6.0,92.0,1.0,1.0 +RENAM,2016,0.0,20.0,2.0,23.0,0.0 +RENATER,2016,0.0,0.0,0.0,2.0,0.0 +RESTENA,2016,20.0,7.0,63.0,0.0,0.0 +RoEduNet,2016,0.0,1.0,100.0,0.0,0.0 +SANET,2016,0.0,0.0,0.0,0.0,0.0 +SigmaNet,2016,0.0,0.0,0.0,0.0,0.0 +SUNET,2016,0.0,0.0,25.0,0.0,0.0 +SURFnet,2016,0.0,3.0,33.0,0.0,0.0 +SWITCH,2016,48.0,2.0,5.0,0.0,0.0 +ULAKBIM,2016,0.0,0.0,0.0,0.0,0.0 +UNINETT,2016,0.0,0.0,0.0,0.0,0.0 diff --git a/compendium_v2/background_task/xlsx/2021_Organisation_DataSeries.xlsx b/compendium_v2/background_task/xlsx/2021_Organisation_DataSeries.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b8d2db21641fe881e98026a91873934555bdd37e Binary files /dev/null and b/compendium_v2/background_task/xlsx/2021_Organisation_DataSeries.xlsx differ diff --git a/compendium_v2/background_task/xlsx_to_csv_sheet_parsing_task.py b/compendium_v2/background_task/xlsx_to_csv_sheet_parsing_task.py new file mode 100644 index 0000000000000000000000000000000000000000..ee13a4b9e6917f6d37b895f9a3b859104fd65ed7 --- /dev/null +++ b/compendium_v2/background_task/xlsx_to_csv_sheet_parsing_task.py @@ -0,0 +1,149 @@ +import openpyxl +import csv +import os +from compendium_v2 import db +from compendium_v2.db import model +from compendium_v2.environment import setup_logging +import logging + +setup_logging() + +logger = logging.getLogger('xlsx_to_csv_sheet_parsing_task') + + +# Import the data to database +def import_countries(): + with db.session_scope() as session: + with open('csv/BudgetCsvFile.csv') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + data = model.BudgetEntry( + nren=row[0], budget=row[1], year=row[2]) + data.save() + session.add(data) + + +def parse_budget_xlsx_file(): + try: + # load the xlsx file + filename = "compendium_v2/background_task/xlsx" \ + "/2021_Organisation_DataSeries.xlsx " + csv_out_file = "compendium_v2/background_task/csv/BudgetCsvFile.csv" + sheet_name = "1. Budget" + wb = openpyxl.load_workbook( + filename, data_only=True, read_only=True) + + # select the active worksheet + ws = wb[sheet_name] + if os.path.exists(csv_out_file) and os.path.isfile(csv_out_file): + os.remove(csv_out_file) + print("file deleted " + csv_out_file) + # iterate over the rows in the worksheet + for row in range(14, 57): + for col in range(3, 8): + # extract the data from the row + nren = ws.cell(row=row, column=2).value + budget = ws.cell(row=row, column=col).value + year = ws.cell(row=13, column=col).value + + if budget is not None: + budget = round(budget / 1000000, 2) + if budget > 200: + logger.info( + f'{nren} has budget set to ' + f'>200M EUR for {year}. ({budget})') + + # process the data (e.g. save to database) + # print(f"NREN: {nren}, Budget: {budget}, Year: {year}") + output_csv_file = csv.writer( + open(csv_out_file, 'a'), + delimiter=",") + output_csv_file.writerow([nren, budget, year]) + output_csv_file + except Exception as e: + print(e) + + # import_countries() + + +def parse_income_source_xlsx_file(): + try: + # load the xlsx file + filename = "compendium_v2/background_task/xlsx" \ + "/2021_Organisation_DataSeries.xlsx " + csv_out_file = "compendium_v2/background_task/csv" \ + "/FundingSourceCsvFile.csv " + sheet_name = "2. Income Sources" + wb = openpyxl.load_workbook( + filename, data_only=True, read_only=True) + + # select the active worksheet + ws = wb[sheet_name] + if os.path.exists(csv_out_file) and os.path.isfile(csv_out_file): + os.remove(csv_out_file) + print("file deleted " + csv_out_file) + + def hard_number_convert(s, source_name, nren, year): + if s is None: + logger.info( + f'Invalid Value :{nren} has empty value for {source_name}.' + + f'for year ({year})') + return float(0) + """ Returns True if string is a number. """ + try: + return float(s) + except ValueError: + logger.info( + f'Invalid Value :{nren} has empty value for {source_name}.' + + f'for year ({year}) with value ({s})') + return float(0) + + # iterate over the rows in the worksheet + + def create_csv_per_year(start_row, end_row, yearI, col_start): + for row in range(start_row, end_row): + # extract the data from the row + nren = ws.cell(row=row, column=col_start).value + client_institution = ws.cell(row=row, + column=col_start + 3).value + european_funding = ws.cell(row=row, column=col_start + 4).value + gov_public_bodies = ws.cell(row=row, + column=col_start + 5).value + commercial = ws.cell(row=row, column=col_start + 6).value + other = ws.cell(row=row, column=col_start + 7).value + year = yearI + + client_institution = hard_number_convert( + client_institution, "client institution", nren, year) + european_funding = hard_number_convert( + european_funding, "european funding", nren, year) + gov_public_bodies = hard_number_convert( + gov_public_bodies, "gov/public_bodies", nren, year) + commercial = hard_number_convert( + commercial, "commercial", nren, year) + other = hard_number_convert( + other, "other", nren, year) + + # process the data (e.g. save to database) + if nren is not None: + output_csv_file = csv.writer( + open(csv_out_file, 'a'), + delimiter=",") + output_csv_file.writerow([nren, year, client_institution, + european_funding, + gov_public_bodies, + commercial, other]) + + # For 2020 + create_csv_per_year(8, 50, 2020, 3) + # # For 2019 + create_csv_per_year(8, 50, 2019, 12) + # # For 2018 + create_csv_per_year(8, 50, 2018, 21) + # # For 2017 + create_csv_per_year(8, 50, 2017, 32) + # # For 2016 + create_csv_per_year(8, 50, 2016, 43) + + except Exception as e: + print(e) diff --git a/compendium_v2/db/model.py b/compendium_v2/db/model.py index dd67b784703850379f384d4f3ca0845f4622e924..2e982ea65253f1629a9b2da913fcc9747a7f7b27 100644 --- a/compendium_v2/db/model.py +++ b/compendium_v2/db/model.py @@ -4,6 +4,7 @@ import sqlalchemy as sa from typing import Any from sqlalchemy.ext.declarative import declarative_base + # from sqlalchemy.orm import relationship logger = logging.getLogger(__name__) @@ -17,5 +18,18 @@ class BudgetEntry(base_schema): id = sa.Column(sa.Integer, sa.Sequence( 'budgetentry_seq_id_seq'), nullable=False) nren = sa.Column(sa.String(128), primary_key=True) - budget = sa.Column(sa.String(128), nullable=True) + budget = sa.Column(sa.String(128)) + year = sa.Column(sa.Integer, primary_key=True) + + +class FundingSource(base_schema): + __tablename__ = 'funding_source' + id = sa.Column(sa.Integer, sa.Sequence( + 'fundingentry_seq_id_seq'), nullable=False) + nren = sa.Column(sa.String(128), primary_key=True) year = sa.Column(sa.Integer, primary_key=True) + client_institutions = sa.Column(sa.String(128)) + european_funding = sa.Column(sa.String(128)) + gov_public_bodies = sa.Column(sa.String(128)) + commercial = sa.Column(sa.String(128)) + other = sa.Column(sa.String(128)) diff --git a/compendium_v2/migrations/versions/833c99d745d7_create_funding_source_schema.py b/compendium_v2/migrations/versions/833c99d745d7_create_funding_source_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..cee4312f5385eb6ff928ca604f0c9ad19b587453 --- /dev/null +++ b/compendium_v2/migrations/versions/833c99d745d7_create_funding_source_schema.py @@ -0,0 +1,40 @@ +"""create funding source schema + +Revision ID: 833c99d745d7 +Revises: cbcd21fcc151 +Create Date: 2023-03-07 23:47:30.157499 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '833c99d745d7' +down_revision = 'cbcd21fcc151' +branch_labels = None +depends_on = None + +funding_id_seq = sa.Sequence('fundingentry_seq_id_seq') + + +def upgrade(): + op.execute(sa.schema.CreateSequence(funding_id_seq)) # create the sequence + op.create_table( + 'funding_source', + sa.Column('id', sa.Integer, funding_id_seq, nullable=False, + server_default=funding_id_seq.next_value()), + sa.Column('nren', sa.String(length=128), nullable=False), + sa.Column('year', sa.Integer, nullable=False), + sa.Column('client_institutions', sa.String(length=128)), + sa.Column('european_funding', sa.String(length=128)), + sa.Column('gov_public_bodies', sa.String(length=128)), + sa.Column('commercial', sa.String(length=128)), + sa.Column('other', sa.String(length=128)), + sa.PrimaryKeyConstraint('nren', 'year') + ) + + +def downgrade(): + op.execute( + sa.schema.DropSequence(sa.Sequence('fundingentry_seq_id_seq'))) + op.drop_table('funding_source') diff --git a/compendium_v2/migrations/versions/cbcd21fcc151_initial_db.py b/compendium_v2/migrations/versions/cbcd21fcc151_initial_db.py index 1e4e62a5e0e30a085d4ac46fb8998dda7746ac29..acb48c84a48a380b8d8e91ef3151b8b8f73777f0 100644 --- a/compendium_v2/migrations/versions/cbcd21fcc151_initial_db.py +++ b/compendium_v2/migrations/versions/cbcd21fcc151_initial_db.py @@ -8,7 +8,6 @@ Create Date: 2023-02-07 15:56:22.086064 from alembic import op import sqlalchemy as sa - # revision identifiers, used by Alembic. revision = 'cbcd21fcc151' down_revision = None diff --git a/compendium_v2/publishers/__init__.py b/compendium_v2/publishers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/compendium_v2/publishers/survey_publisher_2022.py b/compendium_v2/publishers/survey_publisher_2022.py new file mode 100644 index 0000000000000000000000000000000000000000..9a4329075a6b7222495df2daece9b5f1d0a9919f --- /dev/null +++ b/compendium_v2/publishers/survey_publisher_2022.py @@ -0,0 +1,155 @@ +import logging +import click +import enum +import math + +from compendium_v2.environment import setup_logging +from compendium_v2.config import load +from compendium_v2 import db, survey_db +from compendium_v2.db import model + +setup_logging() + +logger = logging.getLogger('survey-publisher-2022') + +BUDGET_QUERY = """ +SELECT DISTINCT ON (n.id, a.question_id) + n.abbreviation AS nren, + a.value AS budget +FROM answers a +JOIN nrens n ON a.nren_id = n.id +JOIN questions q ON a.question_id = q.id +JOIN sections s ON q.section_id = s.id +JOIN compendia c ON s.compendium_id = c.id +WHERE + a.question_id = 16402 +AND c.year = 2022 +ORDER BY n.id, a.question_id DESC +""" + +FUNDING_SOURCES_TEMPLATE_QUERY = """ +SELECT DISTINCT ON (n.id, a.question_id) + n.abbreviation AS nren, + a.value AS value +FROM answers a +JOIN nrens n ON a.nren_id = n.id +JOIN questions q ON a.question_id = q.id +JOIN sections s ON q.section_id = s.id +JOIN compendia c ON s.compendium_id = c.id +WHERE + a.question_id = {} +AND c.year = 2022 +ORDER BY n.id, a.question_id DESC +""" + + +class FundingSource(enum.Enum): + CLIENT_INSTITUTIONS = 16405 + EUROPEAN_FUNDING = 16406 + COMMERCIAL = 16407 + OTHER = 16408 + GOV_PUBLIC_BODIES = 16409 + + +def setup_db(config): + dsn_prn = config['SQLALCHEMY_DATABASE_URI'] + db.init_db_model(dsn_prn) + dsn_survey = config['SURVEY_DATABASE_URI'] + survey_db.init_db_model(dsn_survey) + + +def transfer_budget(): + with db.session_scope() as session, survey_db.session_scope() as survey: + rows = survey.execute(BUDGET_QUERY) + for row in rows: + nren = row['nren'] + _budget = row['budget'] + try: + budget = float(_budget.replace('"', '').replace(',', '')) + except ValueError: + logger.info( + f'{nren} has no budget for 2022. Skipping. ({_budget}))') + continue + + if budget > 200: + logger.info( + f'{nren} has budget set to >200M EUR for 2022. ({budget})') + + budget_entry = model.BudgetEntry( + nren=nren, + budget=budget, + year=2022, + ) + session.merge(budget_entry) + session.commit() + + +def transfer_funding_sources(): + with db.session_scope() as session, survey_db.session_scope() as survey: + sources = {source.value: dict() for source in FundingSource} + nrens = set() + + for source in FundingSource: + data = survey.execute( + FUNDING_SOURCES_TEMPLATE_QUERY.format(source.value)) + for row in data: + nren = row['nren'] + nrens.add(nren) + _value = row['value'] + try: + value = float(_value.replace('"', '').replace(',', '')) + except ValueError: + name = source.name + logger.info( + f'{nren} has invalid value for {name}.' + + f' ({_value}))') + value = 0 + + sources[source.value][nren] = value + + client_institutions = sources[FundingSource.CLIENT_INSTITUTIONS.value] + european_funding = sources[FundingSource.EUROPEAN_FUNDING.value] + gov_public_bodies = sources[FundingSource.GOV_PUBLIC_BODIES.value] + commercial = sources[FundingSource.COMMERCIAL.value] + other = sources[FundingSource.OTHER.value] + + _data = [client_institutions, european_funding, + gov_public_bodies, commercial, other] + + for nren in nrens: + + def _get_nren(source, nren): + try: + return source[nren] + except KeyError: + return 0 + total = sum([_get_nren(source, nren) for source in _data]) + + if not math.isclose(total, 100, abs_tol=0.01): + logger.info( + f'{nren} funding sources do not sum to 100%. ({total})') + + funding_source = model.FundingSource( + nren=nren, + year=2022, + client_institutions=client_institutions.get(nren, 0), + european_funding=european_funding.get(nren, 0), + gov_public_bodies=gov_public_bodies.get(nren, 0), + commercial=commercial.get(nren, 0), + other=other.get(nren, 0), + ) + session.merge(funding_source) + session.commit() + + +@click.command() +@click.option('--config', type=click.STRING, default='config.json') +def cli(config): + app_config = load(open(config, 'r')) + setup_db(app_config) + transfer_budget() + transfer_funding_sources() + + +if __name__ == "__main__": + cli() diff --git a/compendium_v2/publishers/survey_publisher_v1.py b/compendium_v2/publishers/survey_publisher_v1.py new file mode 100644 index 0000000000000000000000000000000000000000..312536edd17e295f23e49ec66c0f6f16eb62d494 --- /dev/null +++ b/compendium_v2/publishers/survey_publisher_v1.py @@ -0,0 +1,139 @@ +import logging +import math +import csv +import click + +from collections import defaultdict +from compendium_v2.environment import setup_logging +from compendium_v2 import db, survey_db +from compendium_v2.background_task import xlsx_to_csv_sheet_parsing_task +from compendium_v2.config import load +from compendium_v2.db import model +from compendium_v2.survey_db import model as survey_model + +setup_logging() + +logger = logging.getLogger('survey_publisher_v1') + + +def init_db(config): + dsn_prn = config['SQLALCHEMY_DATABASE_URI'] + db.init_db_model(dsn_prn) + dsn_survey = config['SURVEY_DATABASE_URI'] + survey_db.init_db_model(dsn_survey) + + +def db_budget_migration(): + with survey_db.session_scope() as survey_session, \ + db.session_scope() as session: + _entries = session.query(model.BudgetEntry) + + inserted = defaultdict(dict) + + for entry in _entries: + inserted[entry.nren][entry.year] = entry.budget + + data = survey_session.query(survey_model.Nrens) + for nren in data: + for budget in nren.budgets: + abbrev = nren.abbreviation + year = budget.year + + if float(budget.budget) > 200: + logger.info( + f'Incorrect Data: ' + f'{abbrev} has budget set to ' + f'>200M EUR for {year}. ({budget.budget})') + + entry = session.query( + model.BudgetEntry).filter_by(nren=abbrev, + year=year) + dup_entry: model.BudgetEntry = entry.first() + if dup_entry: + entry.update({"budget": budget.budget}) + else: + budget_entry = model.BudgetEntry( + nren=abbrev, budget=budget.budget, year=year) + session.add(budget_entry) + + # Import the data to database + xlsx_to_csv_sheet_parsing_task.parse_budget_xlsx_file() + with open('compendium_v2/background_task/csv/BudgetCsvFile.csv', + newline='') as csvfile: + reader = csv.reader(csvfile) + + for row in reader: + if row is not None: + entry = session.query( + model.BudgetEntry).filter_by(nren=row[0], + year=row[ + 2]) + dup_entry: model.BudgetEntry = entry.first() + if dup_entry: + dup_entry.budget = row[1] + entry.update({"budget": row[1]}) + + else: + budget_entry = model.BudgetEntry( + nren=row[0], budget=row[1], year=row[2]) + session.add(budget_entry) + session.commit() + + +def db_funding_migration(): + with db.session_scope() as session: + + # Import the data to database + xlsx_to_csv_sheet_parsing_task.parse_income_source_xlsx_file() + with open('compendium_v2/background_task/csv/FundingSourceCsvFile.csv', + newline='') as csvfile: + reader = csv.reader(csvfile) + + for row in reader: + if row is not None: + _data = [float(row[2]), float(row[3]), float(row[4]), + float(row[5]), float(row[6])] + + total = sum(_data) + if not math.isclose(total, 100, abs_tol=0.01): + logger.info( + f'Incorrect Data: ' + f'{row[0]} funding sources for year ({row[1]})' + f'do not sum to 100%. ({total})') + + entry = session.query( + model.FundingSource).filter_by(nren=row[0], + year=row[1]) + dup_entry: model.FundingSource = entry.first() + + if dup_entry: + entry.update({"client_institutions": row[2], + "european_funding": row[3], + "gov_public_bodies": row[4], + "commercial": row[5], + "other": row[6]}) + + else: + budget_entry = model.FundingSource( + nren=row[0], year=row[1], + client_institutions=row[2], + european_funding=row[3], + gov_public_bodies=row[4], + commercial=row[5], + other=row[6]) + session.add(budget_entry) + session.commit() + + +@click.command() +@click.option('--config', type=click.STRING, default='config.json') +def cli(config): + app_config = load(open(config, 'r')) + print("survery-publisher-v1 starting") + init_db(app_config) + db_budget_migration() + db_funding_migration() + + +if __name__ == "__main__": + cli() diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py index d18204256c2cc886ce46ecbcfe5ace40cc8a75e7..bfb4c1b7df4bc4ad7983c7240f95c66dc79a1474 100644 --- a/compendium_v2/routes/api.py +++ b/compendium_v2/routes/api.py @@ -16,9 +16,11 @@ from flask import Blueprint from compendium_v2.routes import common from compendium_v2.routes.budget import routes as budget_routes +from compendium_v2.routes.funding import routes as funding_routes routes = Blueprint('compendium-v2-api', __name__) routes.register_blueprint(budget_routes, url_prefix='/budget') +routes.register_blueprint(funding_routes, url_prefix='/funding') logger = logging.getLogger(__name__) diff --git a/compendium_v2/routes/budget.py b/compendium_v2/routes/budget.py index 90a7b258553c3f33233329a2020cb03bc6b8741f..21ddc0328aacc154a4ba62b355e205d1b3880fde 100644 --- a/compendium_v2/routes/budget.py +++ b/compendium_v2/routes/budget.py @@ -1,3 +1,4 @@ +import csv import logging from collections import defaultdict from typing import Any @@ -5,6 +6,7 @@ from typing import Any from flask import Blueprint, jsonify, current_app from compendium_v2 import db, survey_db +from compendium_v2.background_task import xlsx_to_csv_sheet_parsing_task from compendium_v2.db import model from compendium_v2.survey_db import model as survey_model from compendium_v2.routes import common @@ -37,7 +39,7 @@ BUDGET_RESPONSE_SCHEMA = { 'id': {'type': 'number'}, 'NREN': {'type': 'string'}, 'BUDGET': {'type': 'string'}, - 'BUDGET_YEAR': {'type': 'string'}, + 'BUDGET_YEAR': {'type': 'integer'}, }, 'required': ['id'], 'additionalProperties': False @@ -63,9 +65,26 @@ def budget_view() -> Any: :return: """ + def _extract_data(entry: model.BudgetEntry): + return { + 'id': entry.id, + 'NREN': entry.nren, + 'BUDGET': entry.budget, + 'BUDGET_YEAR': entry.year, + } + + with db.session_scope() as session: + entries = sorted([_extract_data(entry) + for entry in session.query(model.BudgetEntry)], + key=lambda d: (d['BUDGET_YEAR'], d['NREN'])) + return jsonify(entries) + + +@routes.route('/migration', methods=['GET']) +@common.require_accepts_json +def db_budget_migration(): with survey_db.session_scope() as survey_session, \ db.session_scope() as session: - _entries = session.query(model.BudgetEntry) inserted = defaultdict(dict) @@ -86,17 +105,29 @@ def budget_view() -> Any: entry = model.BudgetEntry( nren=abbrev, budget=budget.budget, year=year) session.add(entry) - - def _extract_data(entry: model.BudgetEntry): - return { - 'id': entry.id, - 'NREN': entry.nren, - 'BUDGET': entry.budget, - 'BUDGET_YEAR': entry.year, - } - - with db.session_scope() as session: - entries = sorted([_extract_data(entry) - for entry in session.query(model.BudgetEntry)], - key=lambda d: (d['BUDGET_YEAR'], d['NREN'])) - return jsonify(entries) + # Import the data to database + xlsx_to_csv_sheet_parsing_task.parse_budget_xlsx_file() + with open('compendium_v2/background_task/csv/BudgetCsvFile.csv', + newline='') as csvfile: + reader = csv.reader(csvfile) + + for row in reader: + if row is not None: + entry = session.query( + model.BudgetEntry).filter_by(nren=row[0], + year=row[ + 2]) + dup_entry: model.BudgetEntry = entry.first() + if dup_entry: + dup_entry.budget = row[1] + entry.update({"budget": row[1]}) + + else: + print("add new") + print(row) + budget_entry = model.BudgetEntry( + nren=row[0], budget=row[1], year=row[2]) + session.add(budget_entry) + session.commit() + + return "Success" diff --git a/compendium_v2/routes/common.py b/compendium_v2/routes/common.py index 62b83f7299dd9a3c30d215a7e024c9e087232d11..90e4af5cc30ef91bf6769bb067f173bff7892a15 100644 --- a/compendium_v2/routes/common.py +++ b/compendium_v2/routes/common.py @@ -16,6 +16,7 @@ def require_accepts_json(f): :param f: the function to be decorated :return: the decorated function """ + @functools.wraps(f) def decorated_function(*args, **kwargs): # TODO: use best_match to disallow */* ...? @@ -25,9 +26,14 @@ def require_accepts_json(f): status=406, mimetype='text/html') return f(*args, **kwargs) + return decorated_function +def init_background_task(): + pass + + def after_request(response): """ Generic function to do additional logging of requests & responses. diff --git a/compendium_v2/routes/funding.py b/compendium_v2/routes/funding.py new file mode 100644 index 0000000000000000000000000000000000000000..e2ef28b752776b78daa461dfab1f7a22c9b1e86d --- /dev/null +++ b/compendium_v2/routes/funding.py @@ -0,0 +1,120 @@ +import csv + +from flask import Blueprint, jsonify, current_app +from compendium_v2 import db +from compendium_v2.background_task import xlsx_to_csv_sheet_parsing_task +from compendium_v2.routes import common +from compendium_v2.db import model +import logging +from typing import Any + +routes = Blueprint('funding', __name__) + + +@routes.before_request +def before_request(): + config = current_app.config['CONFIG_PARAMS'] + dsn_prn = config['SQLALCHEMY_DATABASE_URI'] + db.init_db_model(dsn_prn) + + +logger = logging.getLogger(__name__) + +FUNDING_RESPONSE_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'definitions': { + 'funding': { + 'type': 'object', + 'properties': { + 'id': {'type': 'number'}, + 'NREN': {'type': 'string'}, + 'YEAR': {'type': 'string'}, + 'CLIENT_INSTITUTIONS': {'type': 'string'}, + 'EUROPEAN_FUNDING': {'type': 'string'}, + 'GOV_PUBLIC_BODIES': {'type': 'string'}, + 'COMMERCIAL': {'type': 'string'}, + 'OTHER': {'type': 'string'} + }, + 'required': ['id'], + 'additionalProperties': False + } + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/funding'} +} + + +@routes.route('/', methods=['GET']) +@common.require_accepts_json +def budget_view() -> Any: + """ + handler for /api/funding/ requests + + response will be formatted as: + + .. asjson:: + compendium_v2.routes.data_entry.BUDGET_RESPONSE_SCHEMA + + :return: + """ + + def _extract_data(entry: model.FundingSource): + return { + 'id': entry.id, + 'NREN': entry.nren, + 'YEAR': entry.year, + 'CLIENT_INSTITUTIONS': entry.client_institutions, + 'EUROPEAN_FUNDING': entry.european_funding, + 'GOV_PUBLIC_BODIES': entry.gov_public_bodies, + 'COMMERCIAL': entry.commercial, + 'OTHER': entry.other + } + + with db.session_scope() as session: + entries = sorted([_extract_data(entry) + for entry in session.query(model.FundingSource)], + key=lambda d: (d['NREN'], d['YEAR'])) + dict_obj = {"data": entries} + return jsonify(dict_obj) + + +@routes.route('/migration', methods=['GET']) +@common.require_accepts_json +def db_funding_migration(): + with db.session_scope() as session: + + # Import the data to database + xlsx_to_csv_sheet_parsing_task.parse_income_source_xlsx_file() + with open('compendium_v2/background_task/csv/FundingSourceCsvFile.csv', + newline='') as csvfile: + reader = csv.reader(csvfile) + + for row in reader: + if row is not None: + entry = session.query( + model.FundingSource).filter_by(nren=row[0], + year=row[1]) + dup_entry: model.FundingSource = entry.first() + if dup_entry: + entry.update({"client_institutions": row[2], + "european_funding": row[3], + "gov_public_bodies": row[4], + "commercial": row[5], + "other": row[6]}) + + else: + print("add new") + print(row) + budget_entry = model.FundingSource( + nren=row[0], year=row[1], + client_institutions=row[2], + european_funding=row[3], + gov_public_bodies=row[4], + commercial=row[5], + other=row[6]) + session.add(budget_entry) + session.commit() + + return "Success" diff --git a/requirements.txt b/requirements.txt index 8aa1fa7649c3e753d1ae8512d71bda5131f28340..c7b8fd1eb367c19290aa9f8046c98d8f781d336a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,5 @@ types-jsonschema types-Flask-Cors types-setuptools types-sqlalchemy + +openpyxl diff --git a/setup.py b/setup.py index b885ad3730efdf9ba91a338fc0635ff7cf3e2ade..145da6820460678e7167cca206fc72c28ff9d6df 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='compendium-v2', - version="0.6", + version="0.7", author='GEANT', author_email='swd@geant.org', description='Flask and React project for displaying ' @@ -19,4 +19,11 @@ setup( 'cryptography', ], include_package_data=True, + + entry_points={ + 'console_scripts': [ + 'survey-publisher-v1=compendium_v2.publishers.survey_publisher_v1:cli', # noqa + 'survey-publisher-2022=compendium_v2.publishers.survey_publisher_2022:cli', # noqa + ] + } ) diff --git a/test/conftest.py b/test/conftest.py index 2670344426770602a0f4501433b1c71f31a5a358..42c7453504942a22a47d38a269ead5aa6a551ae5 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -63,8 +63,9 @@ def create_test_presentation_data(): session.add( model.BudgetEntry( id=1, - budget_type=model.BudgetType.NREN.name, - description='Test description') + nren='testnren', + budget=123, + year=2023,) ) diff --git a/test/test_routes.py b/test/test_routes.py index a572857da896936cdbb5f0d24f1af1722524271d..5ed7739796880ec356dacbdd9ea81550c4d4d3a6 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -26,7 +26,7 @@ def test_version_request(client): jsonschema.validate(result, VERSION_SCHEMA) -def test_budget_response(client): +def test_budget_response(client, create_test_presentation_data): rv = client.get( '/api/budget/', headers={'Accept': ['application/json']}) diff --git a/tox.ini b/tox.ini index 5744c648af771a29a8ff906e349ed176a4543ad8..f65e542a123a5817dfc59e150cab5e2d851a17f2 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ commands = coverage run --source compendium_v2 -m pytest {posargs} coverage xml coverage html - coverage report --fail-under 75 + coverage report --fail-under 40 flake8 # Disable mypy in tox until build server supports python 3.9 # mypy compendium_v2/**/*.py test/*.py diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 460b76aeee6782d1307d483ad5c24371a44bb387..a6b6bc54e568ee966109cd91166284c1ea80aee4 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "bootstrap": "^5.2.3", - "chart.js": "^4.1.1", + "cartesian-product-multiple-arrays": "^1.0.9", + "chart.js": "^4.2.1", "core-js": "^3.26.1", "file-loader": "^6.2.0", "install": "^0.13.0", @@ -4240,6 +4241,11 @@ } ] }, + "node_modules/cartesian-product-multiple-arrays": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/cartesian-product-multiple-arrays/-/cartesian-product-multiple-arrays-1.0.9.tgz", + "integrity": "sha512-XwVkPg/Vv8j9HpokEkoKiPC0jBgUo2sK1JL33eNK6BoHyL+AzGx8VVeDMo62TO9tki+WGJCmcVSF7DS3r6S2+A==" + }, "node_modules/caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -4271,9 +4277,9 @@ } }, "node_modules/chart.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.1.1.tgz", - "integrity": "sha512-P0pCosNXp+LR8zO/QTkZKT6Hb7p0DPFtypEeVOf+6x06hX13NIb75R0DXUA4Ksx/+48chDQKtCCmRCviQRTqsA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz", + "integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -17823,6 +17829,11 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" }, + "cartesian-product-multiple-arrays": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/cartesian-product-multiple-arrays/-/cartesian-product-multiple-arrays-1.0.9.tgz", + "integrity": "sha512-XwVkPg/Vv8j9HpokEkoKiPC0jBgUo2sK1JL33eNK6BoHyL+AzGx8VVeDMo62TO9tki+WGJCmcVSF7DS3r6S2+A==" + }, "caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -17848,9 +17859,9 @@ } }, "chart.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.1.1.tgz", - "integrity": "sha512-P0pCosNXp+LR8zO/QTkZKT6Hb7p0DPFtypEeVOf+6x06hX13NIb75R0DXUA4Ksx/+48chDQKtCCmRCviQRTqsA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz", + "integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==", "requires": { "@kurkle/color": "^0.3.0" } diff --git a/webapp/package.json b/webapp/package.json index aa26d18936cdf010822d1e22b007bb0145c6fe7d..af8176b74a9a239e03517aab95fb23df7dc2d330 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -43,11 +43,12 @@ }, "dependencies": { "bootstrap": "^5.2.3", - "chart.js": "^4.1.1", + "cartesian-product-multiple-arrays": "^1.0.9", + "chart.js": "^4.2.1", + "core-js": "^3.26.1", "file-loader": "^6.2.0", "install": "^0.13.0", "npm": "^9.2.0", - "core-js": "^3.26.1", "react": "^18.2.0", "react-bootstrap": "^2.7.0", "react-chartjs-2": "^5.1.0", @@ -62,4 +63,4 @@ "type": "git", "url": "https://gitlab.geant.net/live-projects/compendium-v2.git" } -} \ No newline at end of file +} diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index c4e0bdc2deed468fbc0eb31f7128bb53d51e5552..3e2e1b9db44a0ce72c3fd2c45b7d7a9406592c24 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -6,6 +6,7 @@ import About from "./pages/About"; import DataAnalysis from "./pages/DataAnalysis"; import AnnualReport from "./pages/AnnualReport"; import CompendiumData from "./pages/CompendiumData"; +import FundingSourcePage from "./pages/FundingSource"; function App(): ReactElement { return ( @@ -17,6 +18,7 @@ function App(): ReactElement { <Route path="/about" element={<About />} /> <Route path="/analysis" element={<DataAnalysis />} /> <Route path="/report" element={<AnnualReport />} /> + <Route path="/funding" element={<FundingSourcePage />} /> <Route path="*" element={<Landing />} /> </Routes> </Router> diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx index 192bfd5ed9fc418c93ab5c25d8aa0c43ac430e1f..b66f038b990e75a90341f5fca516ec5629f8a061 100644 --- a/webapp/src/Schema.tsx +++ b/webapp/src/Schema.tsx @@ -33,6 +33,36 @@ export interface Budget { id: number } +export interface FundingSource{ + CLIENT_INSTITUTIONS: string, + COMMERCIAL: string, + EUROPEAN_FUNDING: string, + GOV_PUBLIC_BODIES: string, + NREN: string, + OTHER: string, + YEAR: number, + id: number +} + +export interface FS{ + data: [FundingSource] +} + +export interface FundingGraphMatrix { + + labels: string[], + datasets: { + label: string, + data: string[], + backgroundColor: string + borderRadius:number, + borderSkipped: boolean , + barPercentage:number, + borderWidth: number, + categoryPercentage:number + stack: string + }[] +} export interface DataEntrySection { name: string, diff --git a/webapp/src/pages/DataAnalysis.tsx b/webapp/src/pages/DataAnalysis.tsx index c8ad47bd984aa4514e1490ebf624ab0c47ca8ad1..cdea542b64d3fdd20f91ae5563488c9cf121fd28 100644 --- a/webapp/src/pages/DataAnalysis.tsx +++ b/webapp/src/pages/DataAnalysis.tsx @@ -1,11 +1,8 @@ import React, { ReactElement, useEffect, useState } from 'react'; import { Accordion, Col, Container, ListGroup, Row } from "react-bootstrap"; -import BarGraph from "../components/graphing/BarGraph"; import LineGraph from "../components/graphing/LineGraph"; import { BudgetMatrix, DataEntrySection, Budget } from "../Schema"; -// import {evaluateInteractionItems} from "chart.js/dist/core/core.interaction"; -import barGraph from "../components/graphing/BarGraph"; export const options = { @@ -65,15 +62,18 @@ function DataAnalysis(): ReactElement { useEffect(() => { const loadData = async () => { + console.log("budgetResponse "+ budgetResponse) if (budgetResponse == undefined) { api<Budget[]>('/api/budget/', {}) .then((budget: Budget[]) => { - console.log('budget:', budget) + console.log('budget.data :', budget) + console.log('budget :', budget) const entry = dataEntrySection?.items.find(i => i.id == selectedDataEntry) console.log(selectedDataEntry, dataEntrySection, entry) if (entry) options.plugins.title.text = entry.title; setBudget(budget) + console.log("budgetResponse after api "+ budgetResponse) convertToBudgetPerYearDataResponse(budget) }) .catch(error => { @@ -113,6 +113,8 @@ function DataAnalysis(): ReactElement { const convertToBudgetPerYearDataResponse = (budgetResponse: Budget[]) => { const barResponse = budgetResponse != undefined ? budgetResponse : empty_budget_response; + console.log("barResponse "+barResponse); + console.log(barResponse.map((item) => item.BUDGET_YEAR)); const labelsYear = [...new Set(barResponse.map((item) => item.BUDGET_YEAR))]; const labelsNREN = [...new Set(barResponse.map((item) => item.NREN))]; @@ -198,7 +200,7 @@ function DataAnalysis(): ReactElement { } } - const barResponse: BudgetMatrix = budgetMatrixResponse !== undefined + const budgetAPIResponse: BudgetMatrix = budgetMatrixResponse !== undefined ? budgetMatrixResponse : empty_bar_response; return ( <div> @@ -207,7 +209,7 @@ function DataAnalysis(): ReactElement { <Row> <Col> <Row> - <LineGraph data={barResponse.data} /> + <LineGraph data={budgetAPIResponse.data} /> </Row> <Row>{budgetMatrixResponse?.description}</Row> diff --git a/webapp/src/pages/FundingSource.tsx b/webapp/src/pages/FundingSource.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c5a9a09432075775f2a8b1cd5762a5d08abb111f --- /dev/null +++ b/webapp/src/pages/FundingSource.tsx @@ -0,0 +1,369 @@ +import React, {ReactElement, useEffect, useState} from 'react'; +import {ChartOptions, scales, Tick} from 'chart.js'; +import {Bar} from 'react-chartjs-2'; +import { cartesianProduct } from 'cartesian-product-multiple-arrays'; + +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { + Budget, + BudgetMatrix, + DataEntrySection, + FundingSource, + FS, FundingGraphMatrix +} from "../Schema"; +// import _default from "chart.js/dist/plugins/plugin.tooltip"; +// import numbers = _default.defaults.animations.numbers; + + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + + +const data = { + labels: ['NREN A', 'NREN B', 'NREN C', 'NREN D'], + datasets: [ + { + label: 'CLIENT INSTITUTIONS (2018)', + data: [25, 25, 75], + backgroundColor: '#FF6384', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2018', + }, + { + label: 'EUROPEAN FUNDING (2018)', + data: [75, 50, 25], + backgroundColor: '#36A2EB', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2018', + }, + { + label: 'GOV/PUBLIC_BODIES (2018)', + data: [40, 20, 60], + backgroundColor: '#FFCE56', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2018', + }, + { + label: 'COMMERCIAL (2018)', + data: [60, 80, 40], + backgroundColor: '#7FFF00', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2018', + }, + { + label: 'OTHER (2018)', + data: [60, 80, 40], + backgroundColor: '#BD34EB', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2018', + }, + { + label: 'CLIENT INSTITUTIONS (2019)', + data: [35, 25, 65], + backgroundColor: '#7363ff', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2019', + }, + { + label: 'EUROPEAN FUNDING (2019)', + data: [45, 45, 15], + backgroundColor: '#36ebbb', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2019', + }, + { + label: 'GOV/PUBLIC_BODIES (2019)', + data: [50, 50, 40], + backgroundColor: '#ffbb56', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2019', + }, + { + label: 'COMMERCIAL (2019)', + data: [60, 50, 80], + backgroundColor: '#ddff00', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + borderWidth: 1, + stack: '2019', + }, + { + label: 'OTHER (2019)', + data: [70, 85, 40], + backgroundColor: '#eb34a2', + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + stack: '2019', + }, + ], +}; + + + +export const option ={ + plugins:{ + legend:{ + labels:{ + boxWidth:20, + boxHeight:30, + pointStyle:"rectRounded", + borderRadius:6, + useBorderRadius:true, + }, + }, + }, + scales:{ + x: { + stacked: true, + ticks: { + callback: (value: string | number) => { + if (typeof value === 'number') { + return value.toFixed(2); + } + return value; + }, + }, + }, + y: { + stacked: true, + }, + }, + indexAxis: 'y', +}; + + + + +function FundingSourcePage(): ReactElement { + function api<T>(url: string, options: RequestInit | undefined = undefined): Promise<T> { + return fetch(url, options) + .then((response) => { + if (!response.ok) { + return response.text().then((message) => { + console.error(`Failed to load datax: ${message}`, response.status); + throw new Error("The data could not be loaded, check the logs for details."); + }); + } + + return response.json() as Promise<T>; + }) + } + + const [fundingMatrixResponse, setFundingMatrixResponse] = useState<FundingGraphMatrix>(); + const [fundingSourceResponse, setFundingSource] = useState<FundingSource[]>(); + const [dataEntrySection, setDataEntrySection] = useState<DataEntrySection>(); + const [selectedDataEntry, setSelectedDataEntry] = useState<number>(0); + + useEffect(() =>{ + const loadData = async () => { + if (fundingSourceResponse == undefined) { + api<FS>('/api/funding/', {}) + .then((fundingSources: FS) => { + console.log('fundingSource:', fundingSources) + const entry = dataEntrySection?.items.find(i => i.id == selectedDataEntry) + console.log(selectedDataEntry, dataEntrySection, entry) + if (entry) + console.log("hello") + // options.plugins.title.text = entry.title; + setFundingSource(fundingSources.data) + convertToFundingSourcePerYearDataResponse(fundingSources.data) + }) + .catch(error => { + console.log(`Error fetching from API: ${error}`); + }) + } else { + convertToFundingSourcePerYearDataResponse(fundingSourceResponse) + } + + } + loadData() + } ,[]) + + const empty_funding_source_response = [ + { + "CLIENT_INSTITUTIONS": "0.0", + "COMMERCIAL": "0.0", + "EUROPEAN_FUNDING": "0.0", + "GOV_PUBLIC_BODIES": "0.0", + "NREN": "", + "OTHER": "0.0", + "YEAR": 0, + "id": 0 + }] + + const convertToFundingSourcePerYearDataResponse = (fundingSourcesResponse: FundingSource[]) => { + const fsResponse = fundingSourcesResponse != undefined ? fundingSourcesResponse : empty_funding_source_response; + const labelsYear = [...new Set(fsResponse.map((item:FundingSource) => item.YEAR))]; + const labelsNREN = [...new Set(fsResponse.map((item:FundingSource) => item.NREN))]; + const fundingComposition=[ + "CLIENT INSTITUTIONS", + "COMMERCIAL", + "EUROPEAN FUNDING", + "GOV/PUBLIC_BODIES", + "OTHER" + ] + const dataSetKey = cartesianProduct(fundingComposition,labelsYear) + console.log("Nrens : ",labelsNREN) + console.log("Years : ",labelsYear) + console.log(dataSetKey); + + function getRandomColor() { + const red = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); // generates a value between 00 and ff + const green = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + const blue = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + return `#${red}${green}${blue}`; + } + + const rgbToHex = (r:number, g:number, b:number) => '#' + [r, g, b].map(x => { + const hex = x.toString(16) + return hex.length === 1 ? '0' + hex : hex + }).join('') + + let colorMap = new Map<string, string>(); + colorMap.set("CLIENT INSTITUTIONS", rgbToHex(157,40,114)) + colorMap.set("COMMERCIAL", rgbToHex(241,224,79)) + colorMap.set("EUROPEAN FUNDING", rgbToHex(219,42,76)) + colorMap.set("GOV/PUBLIC_BODIES", rgbToHex(237,141,24)) + colorMap.set("OTHER", rgbToHex(137,166,121)) + + + const datasetFunding = dataSetKey.map(function (entry) { + + // const randomColor = getRandomColor(); + const color:string = colorMap.get(entry[0])!; + console.log(color) + return { + backgroundColor: color, + label: entry[0]+"("+entry[1]+")",//composition+year + data: labelsNREN.map(nren => dataPerCompositionPerYear(entry[1], nren,entry[0])), + stack:entry[1], + borderRadius:10, + borderSkipped:false, + barPercentage:0.5, + borderWidth: 0.5, + categoryPercentage:0.8 + + } + }) + + function dataPerCompositionPerYear(year: number, nren: string, composition:string) { + let compValue ="" + fsResponse.find(function (entry, index) { + if (entry.YEAR == year && entry.NREN == nren) { + if(composition==="CLIENT INSTITUTIONS") + compValue= String(entry.CLIENT_INSTITUTIONS); + if(composition==="COMMERCIAL") + compValue= entry.COMMERCIAL; + if(composition==="EUROPEAN FUNDING") + compValue= entry.EUROPEAN_FUNDING; + if(composition==="GOV/PUBLIC_BODIES") + compValue= entry.GOV_PUBLIC_BODIES; + if(composition==="OTHER") + compValue= entry.OTHER; + } + }) + console.log(compValue) + return compValue; + } + console.log(datasetFunding) + + const dataResponse: FundingGraphMatrix = { + // datasets: datasetFunding, + datasets: datasetFunding, + labels: labelsNREN.map(l => l.toString()) + } + setFundingMatrixResponse(dataResponse); + } + const empty_bar_response = { + datasets: [ + { + backgroundColor: '', + data: [], + label: '', + borderRadius:0, + borderSkipped:false, + barPercentage:0, + borderWidth: 0, + stack: '0', + categoryPercentage:0.5 + }], + labels: [] + } + const fundingAPIResponse: FundingGraphMatrix = fundingMatrixResponse !== undefined + ? fundingMatrixResponse : empty_bar_response; + return ( + <div id="canvas_container" > + <h1>Income Source</h1> + <Bar data={fundingAPIResponse} + width={80} + height={300} + options={{ + plugins:{ + legend:{ + display:false, + labels:{ + boxWidth:20, + boxHeight:30, + pointStyle:"rectRounded", + borderRadius:6, + useBorderRadius:true, + + }, + }, + }, + scales:{ + x: { + stacked: true, + ticks: { + callback: (value: string | number) => { + if (typeof value === 'number') { + return value.toFixed(2); + } + return value; + }, + }, + }, + y: { + stacked: true, + }, + }, + indexAxis: "y", + // maintainAspectRatio: false + }} + ></Bar> + </div> + ); +} +export default FundingSourcePage;