Repository: akvo/akvo-rsr

Issue:
A script for unlocking indicator periods
WASH Alliance is about to start using the Results Framework in December this year and are planning to roll it out on about 80 projects. That would make for about 1000 indicators in total.
Since we won't be able to implement the bulk action feature until then, Annabelle asks to unlock all the indicator periods for them. Otherwise, it will entail a lot of manual work to unlock indicator periods for all the 1000 indicators.

The proposal is to write a script that would unlock indicator periods for all the projects that have 'WASH Alliance' as a keyword. 


Discussion:

Resolving patch:
diff --git a/akvo/rsr/management/commands/lock_unlock_periods.py b/akvo/rsr/management/commands/lock_unlock_periods.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/management/commands/lock_unlock_periods.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Akvo Reporting is covered by the GNU Affero General Public License.
+# See more details in the license.txt file located at the root folder of the Akvo RSR module.
+# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+
+from optparse import make_option
+import sys
+
+from django.core.management.base import BaseCommand
+
+from ...models import IndicatorPeriod, Keyword, Project
+
+
+class Command(BaseCommand):
+
+    args = '<lock|unlock>'
+    help = 'Script for locking and unlocking periods based on Keyword'
+
+    option_list = BaseCommand.option_list + (
+        make_option('-k', '--keyword',
+                    action='store', dest='keyword',
+                    help='Keyword to use for filtering Indicator Periods'),
+        )
+
+    def handle(self, *args, **options):
+
+        # parse options
+        verbosity = int(options['verbosity'])
+        keyword = options['keyword']
+
+        if not len(args) == 1 or args[0].lower() not in ('lock', 'unlock') or not keyword:
+            print 'Usage: {} {} {} --keyword KEYWORD'.format(sys.argv[0], sys.argv[1], self.args)
+            sys.exit(1)
+
+        action = args[0].lower()
+
+        try:
+            keyword = Keyword.objects.get(label=keyword)
+        except Keyword.DoesNotExist:
+            print 'Keyword does not exist'
+            sys.exit(1)
+
+        projects = Project.objects.filter(keywords__in=[keyword])
+        indicator_periods = IndicatorPeriod.objects.filter(indicator__result__project_id__in=projects)
+        count = indicator_periods.count()
+        if count == 0:
+            print 'No indicator periods found to {}'.format(action)
+            sys.exit(0)
+
+        if verbosity > 1:
+            self.stdout.write('{}ing {} periods'.format(action.capitalize(), count))
+
+        locked = action == 'lock'
+        indicator_periods.update(locked=locked)
+        self.stdout.write('{}ed {} periods'.format(action.capitalize(), count))


Test patch:
diff --git a/akvo/rsr/tests/commands/__init__.py b/akvo/rsr/tests/commands/__init__.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/commands/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+"""Akvo RSR is covered by the GNU Affero General Public License.
+
+See more details in the license.txt file located at the root folder of the Akvo RSR module.
+For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+"""
diff --git a/akvo/rsr/tests/commands/test_lock_unlock_periods.py b/akvo/rsr/tests/commands/test_lock_unlock_periods.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/commands/test_lock_unlock_periods.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+"""Akvo RSR is covered by the GNU Affero General Public License.
+
+See more details in the license.txt file located at the root folder of the Akvo RSR module.
+For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+"""
+
+from django.core import management
+from django.test import TestCase
+
+from akvo.rsr.models import Indicator, IndicatorPeriod, Keyword, Project, Result
+
+
+class LockUnlockPeriodsTestCase(TestCase):
+    """Testing that locking and unlocking management command works correctly."""
+
+    def setUp(self):
+        self.tearDown()
+        self.project_ids = []
+        self.unlocked_period_ids = []
+        for i in range(1, 3):
+            project = Project.objects.create(title='Project-{}'.format(i))
+            self.project_ids.append(project.id)
+            keyword = Keyword.objects.create(label='Keyword-{}'.format(i))
+            project.keywords.add(keyword)
+            result = Result.objects.create(project=project)
+            indicator = Indicator.objects.create(result=result)
+            for j in range(1, 3):
+                period = IndicatorPeriod.objects.create(indicator=indicator, locked=j % 2)
+                if not period.locked:
+                    self.unlocked_period_ids.append(period.id)
+
+    def tearDown(self):
+        Project.objects.all().delete()
+        Keyword.objects.all().delete()
+
+    def test_should_not_run_without_keyword(self):
+        """Test that command doesn't run if no keyword is supplied."""
+
+        # When
+        with self.assertRaises(SystemExit):
+            management.call_command('lock_unlock_periods', 'unlock')
+
+    def test_unlocking_periods_works(self):
+        """Test that all matching periods are unlocked."""
+        # When
+        management.call_command('lock_unlock_periods', 'unlock', keyword='Keyword-1')
+
+        # Then
+        project_id = self.project_ids[0]
+        for period in IndicatorPeriod.objects.filter(indicator__result__project_id=project_id):
+            self.assertFalse(period.locked)
+
+        project_id = self.project_ids[1]
+        for period in IndicatorPeriod.objects.filter(indicator__result__project_id=project_id):
+            self.assertEqual(period.locked, period.id not in self.unlocked_period_ids)
+
+    def test_locking_periods_works(self):
+        """Test that all matching periods are locked."""
+        # When
+        management.call_command('lock_unlock_periods', 'lock', keyword='Keyword-2')
+
+        # Then
+        project_id = self.project_ids[0]
+        for period in IndicatorPeriod.objects.filter(indicator__result__project_id=project_id):
+            self.assertEqual(period.locked, period.id not in self.unlocked_period_ids)
+
+        project_id = self.project_ids[1]
+        for period in IndicatorPeriod.objects.filter(indicator__result__project_id=project_id):
+            self.assertTrue(period.locked)