From 285db045952a4d7489c48c476137ade863a45b39 Mon Sep 17 00:00:00 2001 From: Blake Long Date: Fri, 22 Apr 2022 12:49:29 -0600 Subject: [PATCH 001/235] refactored gaitid and fundcode into a FK relationship with program table --- factories/workflow_models.py | 18 +++- .../fixtures/one_program_home_page.yaml | 8 +- indicators/fixtures/one_year_program.yaml | 8 +- .../commands/qa_program_widgets/qa_widgets.py | 10 +- indicators/views/view_utils.py | 2 +- indicators/views/views_program.py | 2 +- tola_management/programadmin.py | 8 +- .../tests/test_programadmin_viewset.py | 6 +- .../commands/migrate_gaitid_fundcode.py | 96 +++++++++++++++++++ .../migrations/0060_auto_20220420_1225.py | 48 ++++++++++ workflow/models.py | 60 +++++++----- .../program_page_program_serializers.py | 7 -- .../program_page_program_serializers.py | 11 --- workflow/tests/test_gaitid.py | 42 ++++++++ workflow/tests/test_model_validators.py | 45 +++++++++ workflow/validators.py | 12 +++ 16 files changed, 329 insertions(+), 54 deletions(-) create mode 100644 workflow/management/commands/migrate_gaitid_fundcode.py create mode 100644 workflow/migrations/0060_auto_20220420_1225.py create mode 100644 workflow/tests/test_gaitid.py create mode 100644 workflow/tests/test_model_validators.py create mode 100644 workflow/validators.py diff --git a/factories/workflow_models.py b/factories/workflow_models.py index 747ddc636..2a0243638 100644 --- a/factories/workflow_models.py +++ b/factories/workflow_models.py @@ -28,6 +28,7 @@ Program, CountryAccess, ProgramAccess, + GaitID, PROGRAM_ROLE_CHOICES, COUNTRY_ROLE_CHOICES ) @@ -123,10 +124,18 @@ def password(obj, create, extracted, **kwargs): obj.user.save() +class GaitIDFactory(DjangoModelFactory): + + class Meta: + model = GaitID + django_get_or_create = ('gaitid',) + + gaitid = Sequence(lambda n: "%0030d" % n) + + class ProgramFactory(DjangoModelFactory): class Meta: model = Program - django_get_or_create = ('gaitid',) class Params: active = True @@ -138,7 +147,7 @@ class Params: ) name = 'Health and Survival for Syrians in Affected Regions' - gaitid = Sequence(lambda n: "%0030d" % n) + gaitid = RelatedFactory(GaitIDFactory, factory_related_name='program') country = RelatedFactory(CountryFactory, country='United States', code='US') funding_status = LazyAttribute(lambda o: "funded" if o.active else "Inactive") _using_results_framework = Program.RF_ALWAYS @@ -177,11 +186,16 @@ class Params: age = False name = Faker('company') + gaitid = RelatedFactory(GaitIDFactory, factory_related_name='program') funding_status = LazyAttribute(lambda o: "Funded" if o.active else "Inactive") _using_results_framework = LazyAttribute( lambda o: Program.RF_ALWAYS if o.migrated is None else Program.MIGRATED if o.migrated else Program.NOT_MIGRATED ) + @post_generation + def gaitid(self, create, extracted, **kwargs): + GaitIDFactory(gaitid='123456', program=self) + @lazy_attribute def reporting_period_start(self): year = datetime.date.today().year diff --git a/indicators/fixtures/one_program_home_page.yaml b/indicators/fixtures/one_program_home_page.yaml index 2b59a8c60..a39b3bc61 100644 --- a/indicators/fixtures/one_program_home_page.yaml +++ b/indicators/fixtures/one_program_home_page.yaml @@ -10,7 +10,6 @@ - model: workflow.program pk: 1 fields: - gaitid: '1' name: Basic One Year Program funding_status: Funded cost_center: null @@ -23,6 +22,13 @@ reporting_period_end: 2016-12-31 sector: [] country: [1] +- model: workflow.gaitid + pk: 1 + fields: + gaitid: '1' + program: 1 + create_date: 2022-04-01 + edit_date: 2022-04-01 - model: indicators.indicator pk: 1 fields: diff --git a/indicators/fixtures/one_year_program.yaml b/indicators/fixtures/one_year_program.yaml index 5fa16dfd9..f06e665f2 100644 --- a/indicators/fixtures/one_year_program.yaml +++ b/indicators/fixtures/one_year_program.yaml @@ -10,7 +10,6 @@ - model: workflow.program pk: 1 fields: - gaitid: '1' name: Basic One Year Program funding_status: Funded cost_center: null @@ -23,3 +22,10 @@ reporting_period_end: 2017-12-31 sector: [] country: [1] +- model: workflow.gaitid + pk: 1 + fields: + gaitid: '1' + program: 1 + create_date: 2022-04-01 + edit_date: 2022-04-01 diff --git a/indicators/management/commands/qa_program_widgets/qa_widgets.py b/indicators/management/commands/qa_program_widgets/qa_widgets.py index b7dcebdfa..e852e6025 100644 --- a/indicators/management/commands/qa_program_widgets/qa_widgets.py +++ b/indicators/management/commands/qa_program_widgets/qa_widgets.py @@ -20,7 +20,7 @@ DisaggregatedValue, LevelTier, ) -from workflow.models import Program, Country, Organization, TolaUser, SiteProfile, Sector +from workflow.models import Program, Country, Organization, TolaUser, SiteProfile, Sector, GaitID from indicators.views.views_indicators import generate_periodic_targets @@ -35,6 +35,10 @@ def __init__(self, country): self.default_start_date = (date.today() + relativedelta(months=-18)).replace(day=1) self.default_end_date = (self.default_start_date + relativedelta(months=+32)).replace(day=1) - timedelta(days=1) + def create_gait_id(self, program_id): + gait_id = GaitID(gaitid=random.randint(1, 9999), program_id=program_id) + gait_id.save() + def create_program( self, name, start_date=False, end_date=False, post_satsuma=True, multi_country=False, create_levels=True): if not start_date: @@ -49,10 +53,12 @@ def create_program( 'reporting_period_start': start_date, 'reporting_period_end': end_date, 'funding_status': 'Funded', - 'gaitid': 'fake_gait_id_{}'.format(random.randint(1, 9999)), '_using_results_framework': Program.RF_ALWAYS if post_satsuma else Program.NOT_MIGRATED, }) program.country.add(self.country) + + self.create_gait_id(program.id) + if multi_country: country2 = Country.objects.get(country="United States") program.country.add(country2) diff --git a/indicators/views/view_utils.py b/indicators/views/view_utils.py index 7e96ae3b4..7a00f253f 100644 --- a/indicators/views/view_utils.py +++ b/indicators/views/view_utils.py @@ -154,7 +154,7 @@ def program_rollup_data(program, for_csv=False): dict = { "unique_id": program.pk, "program_name": program.name, - "gait_id": program.gaitid, + "gait_id": program.gaitids, "countries": " / ".join([c.country for c in program.country.all()]) if for_csv else [c.country for c in program.country.all()], "sectors": " / ".join(set([i.sector.sector for i in program.indicator_set.all() if i.sector and i.sector.sector])) if for_csv else set([i.sector.sector for i in program.indicator_set.all() if i.sector and i.sector.sector]), diff --git a/indicators/views/views_program.py b/indicators/views/views_program.py index a06f0f58e..1592ff79a 100644 --- a/indicators/views/views_program.py +++ b/indicators/views/views_program.py @@ -317,7 +317,7 @@ def get_parent_segment(ontology_segments, level): direction_of_change_map[indicator.direction_of_change] if indicator.direction_of_change else 'None', indicator.lop_target_calculated if indicator.lop_target_calculated else indicator.lop_target, program.name, - program.gaitid if program.gaitid else "no gait_id, program id {}".format(program.id), + program.gaitids if program.gaitids else "no gait_id, program id {}".format(program.id), '/'.join([c.strip() for c in program.countries.split(',')]), '/'.join(set(regions)), 'Active' if program.funding_status == 'Funded' else 'Inactive', diff --git a/tola_management/programadmin.py b/tola_management/programadmin.py index f5a030d2e..014dda557 100644 --- a/tola_management/programadmin.py +++ b/tola_management/programadmin.py @@ -19,7 +19,8 @@ ValidationError, BooleanField, DateTimeField, - SerializerMethodField + SerializerMethodField, + StringRelatedField ) from openpyxl import Workbook, utils @@ -241,7 +242,7 @@ class ProgramAdminSerializer(ModelSerializer): id = IntegerField(allow_null=True, required=False) name = CharField(required=True, max_length=255) funding_status = CharField(required=True) - gaitid = CharField(required=False, allow_blank=True, allow_null=True) + gaitid = StringRelatedField(many=True) fundCode = CharField(required=False, allow_blank=True, allow_null=True, source='cost_center') description = CharField(allow_null=True, allow_blank=True) sector = NestedSectorSerializer(required=True, many=True) @@ -497,7 +498,8 @@ def base_queryset(cls): ), ), to_attr='country_with_users' - ) + ), + 'gaitid' ) return queryset diff --git a/tola_management/tests/test_programadmin_viewset.py b/tola_management/tests/test_programadmin_viewset.py index d61d1a151..514ee20c3 100644 --- a/tola_management/tests/test_programadmin_viewset.py +++ b/tola_management/tests/test_programadmin_viewset.py @@ -40,7 +40,7 @@ def test_base_program_info(self): data = ProgramAdminSerializer(queryset, many=True).data[0] self.assertEqual(data['name'], SPECIAL_CHARS) self.assertEqual(data['funding_status'], 'funded') - self.assertEqual(data['gaitid'], '123456') + self.assertEqual(data['gaitid'][0], '123456') self.assertEqual(data['description'], 'A description') self.assertEqual(data['id'], program.pk) @@ -207,7 +207,7 @@ def get_program(self, program_pk): def get_data(self, program_qs): # 3 queries: program with annotations, prefetched sectors, prefetched countries - with self.assertNumQueries(6): + with self.assertNumQueries(7): return ProgramAdminSerializer(program_qs, many=True).data[0] def get_users_filtered_data(self, program_pk): @@ -276,7 +276,7 @@ def test_with_superusers_program(self): self.assertEqual(users_data['count'], 14) def test_query_count_on_multiple(self): - with self.assertNumQueries(7): + with self.assertNumQueries(8): program_qs = ProgramAdminViewSet.base_queryset().all() data = ProgramAdminSerializer(program_qs, many=True).data self.assertEqual(len(data), 25) diff --git a/workflow/management/commands/migrate_gaitid_fundcode.py b/workflow/management/commands/migrate_gaitid_fundcode.py new file mode 100644 index 000000000..5614e1788 --- /dev/null +++ b/workflow/management/commands/migrate_gaitid_fundcode.py @@ -0,0 +1,96 @@ +from django.core.exceptions import ValidationError +from workflow.models import Program, GaitID, FundCode +from django.core.management.base import BaseCommand +from django.db.utils import IntegrityError + + +class Command(BaseCommand): + """ + Command for moving saved gaitids under Program to a new GaitID table. + """ + help = "Moves gaitids saved under Program to their own GaitID table" + + def add_arguments(self, parser): + parser.add_argument('--execute', action='store_true', help='Without this flag, the command will only be a dry run') + parser.add_argument('--supress_output', action='store_true', help='Supresses the output so tests don\'t get too messy') + parser.add_argument('--clean', action='store_true', help='Cleans the database. Deletes GaitIDs and FundCodes created by execute.') + + def valid_int(self, value): + if value is None: + return False + try: + int(value) + return True + except ValueError: + return False + + + def handle(self, *args, **options): + if not options['execute'] and not options['supress_output'] and not options['clean']: + print('Dry Run') + + if options['clean']: + print('Cleaning the database') + + programs = Program.objects.all() + counts = { + 'gaitid': { + 'created': 0, + 'skipped': 0, + 'deleted': 0, + 'invalid': 0 + }, + 'fund_code': { + 'created': 0, + 'skipped': 0, + 'deleted': 0, + 'invalid': 0 + } + } + + if options['execute']: + for program in programs: + if self.valid_int(program.legacy_gaitid): + try: + gait_id = GaitID(program=program, gaitid=program.legacy_gaitid) + gait_id.save() + + counts['gaitid']['created'] += 1 + + if self.valid_int(program.cost_center): + try: + fund_code = FundCode(gaitid=gait_id, fund_code=program.cost_center) + fund_code.save() + counts['fund_code']['created'] += 1 + except IntegrityError: + counts['fund_code']['skipped'] += 1 + except ValidationError: + counts['fund_code']['invalid'] += 1 + else: + counts['fund_code']['invalid'] +=1 + + # Integrity error is raised when the unique_together constraint placed on program, gaitid fails + except IntegrityError: + counts['gaitid']['skipped'] += 1 + continue + else: + counts['gaitid']['invalid'] += 1 + + + if options['clean']: + gait_ids = GaitID.objects.all() + for gait_id in gait_ids: + gait_id.delete() + counts['gaitid']['deleted'] += 1 + # FundCodes get deleted if GaitID is deleted + counts['fund_code']['deleted'] += 1 + + if not options['supress_output']: + print('GAIT ids created: ', counts['gaitid']['created']) + print('Duplicate GAIT ids skipped: ', counts['gaitid']['skipped']) + print('GAIT ids deleted: ', counts['gaitid']['deleted']) + print('Gait ids invalid: ', counts['gaitid']['invalid']) + print('Fund codes created: ', counts['fund_code']['created']) + print('Duplicate fund codes skipped: ', counts['fund_code']['skipped']) + print('Fund codes deleted: ', counts['fund_code']['deleted']) + print('Fund codes invalid: ', counts['fund_code']['invalid']) diff --git a/workflow/migrations/0060_auto_20220420_1225.py b/workflow/migrations/0060_auto_20220420_1225.py new file mode 100644 index 000000000..13662d44c --- /dev/null +++ b/workflow/migrations/0060_auto_20220420_1225.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.12 on 2022-04-20 19:25 + +from django.db import migrations, models +import django.db.models.deletion +import workflow.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflow', '0059_program_external_program_id'), + ] + + operations = [ + migrations.RenameField( + model_name='program', + old_name='gaitid', + new_name='legacy_gaitid', + ), + migrations.CreateModel( + name='GaitID', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gaitid', models.IntegerField()), + ('donor', models.TextField(null=True)), + ('donor_dept', models.TextField(null=True)), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('edit_date', models.DateTimeField(auto_now=True)), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gaitid', to='workflow.program')), + ], + options={ + 'unique_together': {('gaitid', 'program')}, + }, + ), + migrations.CreateModel( + name='FundCode', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fund_code', models.IntegerField(validators=[workflow.validators.validate_fund_code])), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('edit_date', models.DateTimeField(auto_now=True)), + ('gaitid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflow.gaitid')), + ], + options={ + 'unique_together': {('fund_code', 'gaitid')}, + }, + ), + ] diff --git a/workflow/models.py b/workflow/models.py index 720720b4c..62fa6af44 100755 --- a/workflow/models.py +++ b/workflow/models.py @@ -8,7 +8,7 @@ import uuid from django.utils.translation import ugettext_lazy as _ - +from workflow import validators from django.conf import settings from django.db.models import Count, Max, Min, Subquery, OuterRef, Q from django.db.models.signals import post_save @@ -516,11 +516,11 @@ class Program(models.Model): MIGRATED = 2 # programs created before satsuma which have switched to new RF levels RF_ALWAYS = 3 # programs created after satsuma release - on new RF levels with no option - gaitid = models.CharField(_("ID"), max_length=255, null=True, blank=True) + legacy_gaitid = models.CharField(_("ID"), max_length=255, null=True, blank=True) external_program_id = models.IntegerField(_('External program id'), null=True, blank=False) name = models.CharField(_("Program Name"), max_length=255, blank=True) funding_status = models.CharField(_("Funding Status"), max_length=255, blank=True) - cost_center = models.CharField(_("Fund Code"), max_length=255, blank=True, null=True) + cost_center = models.CharField(_("Fund Code"), max_length=255, blank=True, null=True) # Deprecated use program.gaitid.fund_code description = models.TextField(_("Program Description"), max_length=765, null=True, blank=True) sector = models.ManyToManyField(Sector, blank=True, verbose_name=_("Sector")) create_date = models.DateTimeField(null=True, blank=True) @@ -613,29 +613,18 @@ def program_page_url(self): """ return reverse('program_page', kwargs={'program': self.pk}) - @property - def gait_url(self): - """if program has a gait ID, returns url https://gait.mercycorps.org/editgrant.vm?GrantID=#### - otherwise returns false - """ - if self.gaitid is None: - return None - - try: - gaitid = int(self.gaitid) - except ValueError: - gaitid = False - if gaitid and gaitid != 0 and len(str(gaitid)) > 2 and len(str(gaitid)) < 5: - # gaitid exists, is numeric, is nonzero, and is a 3 or 4 digit number: - return 'https://gait.mercycorps.org/editgrant.vm?GrantID={gaitid}'.format( - gaitid=gaitid) - return None - def get_sites(self): indicator_ids = Indicator.objects.filter(program__in=[self.id]).values_list('id') results = Result.objects.filter(indicator__id__in=indicator_ids) return SiteProfile.objects.filter(result__id__in=results).distinct() + @property + def gaitids(self): + """ + Property to help with backwards compatibility with new GaitID table + """ + return list(self.gaitid.values_list('gaitid')) + @property def collected_record_count(self): return Program.objects.filter(pk=self.pk).annotate(num_data=Count('indicator__result')) \ @@ -707,7 +696,7 @@ def target_frequencies(self): @property def admin_logged_fields(self): return { - 'gaitid': self.gaitid, + 'gaitid': self.gaitids, 'name': self.name, 'funding_status': self.funding_status, 'cost_center': self.cost_center, @@ -767,6 +756,33 @@ def manual_numbering(self): return not self.results_framework or not self.auto_number_indicators +class GaitID(models.Model): + gaitid = models.IntegerField() + program = models.ForeignKey(Program, on_delete=models.CASCADE, related_name='gaitid') + donor = models.TextField(null=True) + donor_dept = models.TextField(null=True) + create_date = models.DateTimeField(auto_now_add=True) + edit_date = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ['gaitid', 'program'] + + def __str__(self): + return str(self.gaitid) + + +class FundCode(models.Model): + fund_code = models.IntegerField(validators=[validators.validate_fund_code]) + gaitid = models.ForeignKey(GaitID, on_delete=models.CASCADE) + create_date = models.DateTimeField(auto_now_add=True) + edit_date = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ['fund_code', 'gaitid'] + + def __str__(self): + return str(self.fund_code) + PROGRAM_ROLE_CHOICES = ( # Translators: Refers to a user permission role with limited access to view data only diff --git a/workflow/serializers_new/program_page_program_serializers.py b/workflow/serializers_new/program_page_program_serializers.py index 1af090c16..396502077 100644 --- a/workflow/serializers_new/program_page_program_serializers.py +++ b/workflow/serializers_new/program_page_program_serializers.py @@ -58,7 +58,6 @@ class ProgramPageMixin: needs_additional_target_periods = serializers.BooleanField() site_count = serializers.ReadOnlyField(source='num_sites') has_levels = serializers.SerializerMethodField() - gait_url = serializers.CharField() target_period_info = serializers.SerializerMethodField() class Meta: @@ -67,17 +66,11 @@ class Meta: 'needs_additional_target_periods', 'site_count', 'has_levels', - 'gait_url', 'target_period_info', ] # Class methods used to instantiate serializer with queryset and context to minimize queries - @classmethod - def _get_query_fields(cls): - """Extends parent's list of DB fields needed for included fields""" - return super()._get_query_fields() + ['gaitid'] - @classmethod def get_queryset(cls, **kwargs): """Loads program with annotations specific for program page display (note program_page_objects manager)""" diff --git a/workflow/tests/serializer_tests/program_page_program_serializers.py b/workflow/tests/serializer_tests/program_page_program_serializers.py index 0d402769b..3ae42677e 100644 --- a/workflow/tests/serializer_tests/program_page_program_serializers.py +++ b/workflow/tests/serializer_tests/program_page_program_serializers.py @@ -128,17 +128,6 @@ def test_level_count_nonzero(self): data = self.get_program_data(tiers=['Tier1', 'Tier2'], levels=2) self.assertTrue(data['has_levels']) - def test_gait_url_empty(self): - data = self.get_program_data(gaitid=None) - self.assertEqual(data['gait_url'], None) - - def test_gait_url(self): - data = self.get_program_data(gaitid=1423) - self.assertEqual( - data['gait_url'], - 'https://gait.mercycorps.org/editgrant.vm?GrantID=1423' - ) - def test_time_period_info_non_time_aware(self): p = RFProgramFactory() for frequency in [Indicator.LOP, Indicator.MID_END]: diff --git a/workflow/tests/test_gaitid.py b/workflow/tests/test_gaitid.py new file mode 100644 index 000000000..c939bf2af --- /dev/null +++ b/workflow/tests/test_gaitid.py @@ -0,0 +1,42 @@ +from factories.workflow_models import ProgramFactory +from workflow.models import GaitID +from django.db import transaction +from django.test import TestCase + + +class GaitIDTests(TestCase): + + def setUp(self): + self.program = ProgramFactory() + return super().setUp() + + def get_program_gaitid_count(self): + return GaitID.objects.filter(program=self.program).count() + + def create_gaitids(self, gaitid_list): + for gaitid in gaitid_list: + try: + with transaction.atomic(): + gait = GaitID(program=self.program, gaitid=gaitid) + gait.save() + except ValueError: + continue + + def test_one_gaitid(self): + expected_gaitid_count = 1 + + self.assertEqual(self.get_program_gaitid_count(), expected_gaitid_count) + + def test_multiple_gaitids(self): + gaitid_list = ['1234', '4555', '9743', '3456'] + expected_gaitid_count = 5 # 1 from the ProgramFactory and 4 from the list + self.create_gaitids(gaitid_list) + + self.assertEqual(self.get_program_gaitid_count(), expected_gaitid_count) + + def test_multiple_gaitids_some_invalid(self): + gaitid_list = ['abc', '1234', '8643', '1-a'] + expected_gaitid_count = 3 + self.create_gaitids(gaitid_list) + + self.assertEqual(self.get_program_gaitid_count(), expected_gaitid_count) diff --git a/workflow/tests/test_model_validators.py b/workflow/tests/test_model_validators.py new file mode 100644 index 000000000..b9e356c4a --- /dev/null +++ b/workflow/tests/test_model_validators.py @@ -0,0 +1,45 @@ +from factories.workflow_models import ProgramFactory +from django.core.exceptions import ValidationError +from workflow.models import FundCode, GaitID +from django.test import TestCase + + +class FundCodeValidatorTest(TestCase): + + def setUp(self): + self.program = ProgramFactory() + self.gaitid = GaitID.objects.get(program_id=self.program.id) + return super().setUp() + + def fund_code_len(self): + return FundCode.objects.filter(gaitid=self.gaitid).count() + + def test_invalid_fund_code(self): + invalid_fund_codes = [45000, 'abc', 0.245, '12345'] + + for invalid_fund_code in invalid_fund_codes: + fund_code = FundCode(gaitid=self.gaitid, fund_code=invalid_fund_code) + + self.assertRaises(ValidationError, fund_code.full_clean) + self.assertEqual(self.fund_code_len(), 0) + + def test_valid_fund_code(self): + valid_fund_code = 35000 + fund_code = FundCode(gaitid=self.gaitid, fund_code=valid_fund_code) + + try: + fund_code.full_clean() + fund_code.save() + except ValidationError: + self.fail('Received ValidationError on FundCodeValidatorTest.test_valid_fund_code') + + self.assertEqual(self.fund_code_len(), 1) + + def test_multiple_valid_fund_code(self): + valid_fund_codes = [33000, 34000, 35000, 74000, 92000] + + for valid_fund_code in valid_fund_codes: + fund_code = FundCode(gaitid=self.gaitid, fund_code=valid_fund_code) + fund_code.save() + + self.assertEqual(self.fund_code_len(), 5) diff --git a/workflow/validators.py b/workflow/validators.py new file mode 100644 index 000000000..56cef1f8b --- /dev/null +++ b/workflow/validators.py @@ -0,0 +1,12 @@ +from django.core.exceptions import ValidationError + + +def validate_fund_code(value): + fund_code_ranges = [range(30000, 39999), range(70000, 79000), range(90000, 99999)] + + for fund_code_range in fund_code_ranges: + if value in fund_code_range: + return True + + # Not sure if this error should be translated. With the automatic program creation this error would only be present on the backend. + raise ValidationError('Received invalid fund code') \ No newline at end of file From 890b679b666b9c5fd6e26dcef5a3d742393ef135 Mon Sep 17 00:00:00 2001 From: Andre Thomas Date: Mon, 9 May 2022 14:22:24 -0400 Subject: [PATCH 002/235] For mercycorps/TolaActivity#2769, removed the add program button for all except demo and django admin. --- js/pages/tola_management_pages/program/views.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/js/pages/tola_management_pages/program/views.js b/js/pages/tola_management_pages/program/views.js index a5b4ede4d..b6f795df9 100644 --- a/js/pages/tola_management_pages/program/views.js +++ b/js/pages/tola_management_pages/program/views.js @@ -207,11 +207,13 @@ export const IndexView = observer(
-
- store.createProgram()}> - {gettext("Add Program")} - -
+ {window.location.href.toLowerCase().includes('demo') && + + }
From 8cfb3d40d2528abafd1be2bc8ecf9d3b23bb5a85 Mon Sep 17 00:00:00 2001 From: Andre Thomas Date: Mon, 9 May 2022 15:19:48 -0400 Subject: [PATCH 003/235] removed gait_url from backend serializer, model, and tests. --- templates/indicators/program_page.html | 4 ++-- workflow/models.py | 18 ------------------ .../program_page_program_serializers.py | 2 -- .../program_page_program_serializers.py | 11 ----------- 4 files changed, 2 insertions(+), 33 deletions(-) diff --git a/templates/indicators/program_page.html b/templates/indicators/program_page.html index 83d95645d..3e0ac52d0 100755 --- a/templates/indicators/program_page.html +++ b/templates/indicators/program_page.html @@ -37,10 +37,10 @@

{% trans "Program details" %}

{% endif %}
  • {% trans "Change log" %}
  • {% trans "Indicator plan" %}
  • - {% if program.gait_url %} +