Repository: akvo/akvo-rsr

Issue:
Feature Request: Script to bulk change indicators to cumulative reporting
### What are you trying to do?

I want to change indicators on a project to cumulative report indicators

### Describe the solution you'd like

Create a Django management command

### Have you consider alternatives?

_No response_

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/akvo/rsr/management/commands/switch_indicators_to_cumulative_reporting.py b/akvo/rsr/management/commands/switch_indicators_to_cumulative_reporting.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/management/commands/switch_indicators_to_cumulative_reporting.py
@@ -0,0 +1,51 @@
+import argparse
+from io import TextIOBase
+from django.core.management.base import BaseCommand
+from django.db import transaction
+from akvo.rsr.models import Project, Indicator
+from akvo.rsr.models.result.utils import QUANTITATIVE
+
+
+class Command(BaseCommand):
+    help = "Switch all quantitative indicators of the given project and its descendants to cumulative reporting"
+
+    def add_arguments(self, parser: argparse.ArgumentParser):
+        parser.add_argument("project_id", type=int)
+        parser.add_argument(
+            "--dry-run", action="store_true", help="Don not actually apply the changes"
+        )
+
+    def handle(self, *args, **options):
+        project = Project.objects.get(id=int(options["project_id"]))
+        runner = CommandRunner(self.stdout, project, options["dry_run"])
+
+        try:
+            runner.run()
+        except InterruptedError:
+            self.stdout.write("Changes not applied\n")
+        else:
+            self.stdout.write("Changes applied\n")
+        self.stdout.write("DONE!\n")
+
+
+class CommandRunner:
+    def __init__(self, stdout: TextIOBase, project: Project, dry_run=False):
+        self.stdout = stdout
+        self.project = project
+        self.dry_run = dry_run
+
+    @transaction.atomic
+    def run(self):
+        indicators = Indicator.objects.filter(
+            result__project__in=self.project.descendants(),
+            type=QUANTITATIVE,
+            parent_indicator__isnull=True,
+        )
+        self.stdout.write(
+            f"Switching {indicators.count()} indicators to cumulative reporting\n"
+        )
+        for indicator in indicators:
+            indicator.cumulative = True
+            indicator.save()
+        if self.dry_run:
+            raise InterruptedError()


Test patch:
diff --git a/akvo/rsr/tests/commands/test_switch_indicators_to_cumulative_reporting.py b/akvo/rsr/tests/commands/test_switch_indicators_to_cumulative_reporting.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/commands/test_switch_indicators_to_cumulative_reporting.py
@@ -0,0 +1,118 @@
+from datetime import date
+from akvo.rsr.management.commands.switch_indicators_to_cumulative_reporting import (
+    Command,
+)
+from akvo.rsr.management.commands.tests.base import BaseCommandTestCase
+from akvo.rsr.tests.utils import ProjectFixtureBuilder
+from akvo.rsr.models import Indicator
+from akvo.rsr.models.result.utils import QUALITATIVE, QUANTITATIVE
+
+
+class CommandTestCase(BaseCommandTestCase[Command]):
+    INDICATOR_1 = "Indicator #1"
+    INDICATOR_2 = "Indicator #2"
+    INDICATOR_3 = "Indicator #3"
+    INDEPENDENT_INDICATOR = "Independent indicator"
+    CONTRIBUTOR_1 = "Contributor #1"
+
+    command_class = Command
+
+    def setUp(self):
+        super().setUp()
+        user = self.create_user("test1@akvo.org", "password", is_admin=True)
+        self.project = (
+            ProjectFixtureBuilder()
+            .with_results(
+                [
+                    {
+                        "title": "Result #1",
+                        "indicators": [
+                            {
+                                "title": self.INDICATOR_1,
+                                "type": QUANTITATIVE,
+                                "cumulative": False,
+                                "periods": [
+                                    {
+                                        "period_start": date(2020, 1, 1),
+                                        "period_end": date(2020, 12, 31),
+                                    }
+                                ],
+                            },
+                            {
+                                "title": self.INDICATOR_2,
+                                "type": QUANTITATIVE,
+                                "cumulative": False,
+                                "periods": [
+                                    {
+                                        "period_start": date(2020, 1, 1),
+                                        "period_end": date(2020, 12, 31),
+                                    }
+                                ],
+                            },
+                            {
+                                "title": self.INDICATOR_3,
+                                "type": QUALITATIVE,
+                                "periods": [
+                                    {
+                                        "period_start": date(2020, 1, 1),
+                                        "period_end": date(2020, 12, 31),
+                                    }
+                                ],
+                            },
+                        ],
+                    }
+                ]
+            )
+            .with_contributors([{"title": self.CONTRIBUTOR_1}])
+            .build()
+        )
+        self.contributor = self.project.get_contributor(title=self.CONTRIBUTOR_1)
+
+        # simulate indicator that has an update
+        self.contributor.get_period(indicator__title=self.INDICATOR_1).add_update(
+            user=user
+        )
+
+        # simulate indicator in the contributor project that doesn't inherit from the lead project
+        contrib_result = self.contributor.results.first()
+        Indicator.objects.create(
+            result=contrib_result,
+            title=self.INDEPENDENT_INDICATOR,
+            type=QUANTITATIVE,
+            cumulative=False,
+        )
+
+        self.c.login(username=user.email, password="password")
+
+    def test_run(self):
+        self.run_command(str(self.project.object.id))
+
+        self.assertTrue(self.project.indicators.get(title=self.INDICATOR_1).cumulative)
+        self.assertTrue(self.project.indicators.get(title=self.INDICATOR_2).cumulative)
+        self.assertFalse(self.project.indicators.get(title=self.INDICATOR_3).cumulative)
+
+        self.assertTrue(
+            self.contributor.indicators.get(title=self.INDICATOR_1).cumulative
+        )
+        self.assertTrue(
+            self.contributor.indicators.get(title=self.INDICATOR_2).cumulative
+        )
+        self.assertFalse(
+            self.contributor.indicators.get(title=self.INDICATOR_3).cumulative
+        )
+        self.assertTrue(
+            self.contributor.indicators.get(title=self.INDEPENDENT_INDICATOR).cumulative
+        )
+
+    def test_dry_run(self):
+        self.run_command(str(self.project.object.id), "--dry-run")
+
+        self.assertFalse(self.project.indicators.get(title=self.INDICATOR_1).cumulative)
+        self.assertFalse(self.project.indicators.get(title=self.INDICATOR_2).cumulative)
+
+        self.assertFalse(
+            self.contributor.indicators.get(title=self.INDICATOR_1).cumulative
+        )
+        self.assertFalse(
+            self.contributor.indicators.get(title=self.INDICATOR_2).cumulative
+        )