diff --git a/EquiTrack/EquiTrack/urls.py b/EquiTrack/EquiTrack/urls.py index 1f18f3086b..3275581ee4 100644 --- a/EquiTrack/EquiTrack/urls.py +++ b/EquiTrack/EquiTrack/urls.py @@ -105,7 +105,7 @@ url(r'^partnerships', login_required(PartnershipsView.as_view()), name='partnerships_dashboard'), url(r'^map/$', login_required(MapView.as_view()), name='map'), url(r'^cmt/$', login_required(CmtDashboardView.as_view()), name='cmt'), - url(r'^hact/$', login_required(HACTDashboardView.as_view()), name='hact_dashboard'), + #url(r'^hact/$', login_required(HACTDashboardView.as_view()), name='hact_dashboard'), url(r'^locations/', include('locations.urls')), url(r'^management/', include('management.urls')), diff --git a/EquiTrack/EquiTrack/util_scripts.py b/EquiTrack/EquiTrack/util_scripts.py new file mode 100644 index 0000000000..88cf6d0a3b --- /dev/null +++ b/EquiTrack/EquiTrack/util_scripts.py @@ -0,0 +1,382 @@ +from __future__ import print_function +from django.db import connection +from django.db.models import Count +import time +from datetime import datetime, timedelta +from users.models import Country +from reports.models import ResultType, Result, CountryProgramme, Indicator, ResultStructure +from partners.models import FundingCommitment + +def printtf(*args): + print([arg for arg in args]) + f = open('mylogs.txt','a') + print([arg for arg in args], file=f) + f.close() + +def set_country(name): + connection.set_tenant(Country.objects.get(name=name)) + + +def fix_duplicate_indicators(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + printtf("Fixing duplicate indicators for {}".format(country_name)) + fattrs = ["ramindicator_set"] + + def fix_indicator_code(): + printtf('cleaning codes of indicators for country ', country_name) + indicators = Indicator.objects.filter(code__exact='').all() + for ind in indicators: + ind.code = None + ind.save() + time.sleep(3) + fix_indicator_code() + + def relates_to_anything(cobj): + for a in fattrs: + if getattr(cobj, a).count(): + printtf(cobj.id, cobj, "relates to ", a) + return True + return False + def update_relationships(dpres, keep): + for a in fattrs: + objs = getattr(dpres, a).all() + if len(objs): + for obj in objs: + obj.indicator = keep + obj.save() + printtf("saved obj.id={} obj {} with keepid{} keep {}".format(obj.id, obj, keep.id, keep)) + + def _run_clean(dupes): + printtf(len(dupes), dupes) + for dup in dupes: + dupresults = Indicator.objects.filter(code=dup['code'], result=dup['result']).all() + delq = [] + keep = None + for dpres in dupresults: + if not keep: + keep = dpres + continue + else: + error = False + if relates_to_anything(dpres): + try: + update_relationships(dpres, keep) + except Exception as exp: + printtf('Cannot remove Object {}, id={}'.format(dpres, dpres.id)) + error = True + if error: + printtf("ERROR OCCURED ON RECORD", dpres.id, dpres) + continue + delq.append(dpres) + if not len(delq): + printtf("Nothing is getting removed for {}".format(dupes)) + else: + # delete everyting in the queue + [i.delete() for i in delq] + printtf("deleting: ", delq) + + dupes = Indicator.objects.values('code', 'result').order_by('code', 'result').annotate(Count('pk')).filter(pk__count__gt=1, ram_indicator=True).all() + _run_clean(dupes) + +def fix_duplicate_results(country_name): + + if not country_name: + printtf("country name required /n") + set_country(country_name) + printtf("Fixing duplicate Results for {}".format(country_name)) + # foreign attributes + fattrs = ["governmentinterventionresult_set", + "indicator_set", + "ramindicator_set", + "resultchain_set", + "tripfunds_set"] + fattrs_mapping = { + "governmentinterventionresult_set": "result", + "indicator_set" : "result", + "ramindicator_set": "result", + "resultchain_set": "result", + "tripfunds_set": "wbs" + } + def fix_string_wbs(): + results = Result.objects.filter(wbs__exact='').all() + for res in results: + res.wbs = None + res.save() + fix_string_wbs() + + def reparent_children(current_object, new_parent): + for child in current_object.get_children(): + child.parent = new_parent + printtf( "reparenting child", child.id, child, new_parent.id, new_parent) + child.save() + def relates_to_anything(cobj): + for a in fattrs: + if getattr(cobj, a).count(): + printtf(cobj.id, cobj, "relates to ", a) + return True + return False + def update_relationships(dpres, keep): + for a in fattrs: + objs = getattr(dpres, a).all() + if len(objs): + for obj in objs: + if a == "tripfunds_set": + obj.wbs = keep + else: + obj.result = keep + obj.save() + printtf("saved obj.id={} obj {} with keepid{} keep {}".format(obj.id, obj, keep.id, keep)) + def _run_clean(prop, dupes): + printtf(len(dupes), dupes) + search_string = prop + '__exact' + for dup in dupes: + dupresults = Result.objects.prefetch_related('result_type').filter(**{search_string: dup[prop]}).all() + delq = [] + keep = None + for dpres in dupresults: + if not keep: + keep = dpres + continue + else: + error = False + if dpres.get_children(): + try: + reparent_children(dpres, keep) + except Exception as exp: + printtf('Cannot reparent from Object {}, id={}'.format(dpres, dpres.id)) + error = True + if relates_to_anything(dpres): + try: + update_relationships(dpres, keep) + except Exception as exp: + printtf('Cannot remove Object {}, id={}'.format(dpres, dpres.id)) + error = True + if error: + printtf("ERROR OCCURED ON RECORD", dpres.id, dpres) + continue + delq.append(dpres) + time.sleep(0.3) + if not len(delq): + printtf("Nothing is getting removed for {}".format(dupes)) + else: + # delete everyting in the queue + [i.delete() for i in delq] + printtf("deleting: ", delq) + # get all duplicates that have the same wbs + dupes = Result.objects.values('wbs').annotate(Count('wbs')).order_by().filter(wbs__count__gt=1, wbs__isnull=False).exclude(wbs__exact='').all() + dupes = sorted(dupes, key=lambda x: x['wbs'].count('/'), reverse=True) + _run_clean('wbs', dupes) + +def cp_fix(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + printtf("Fixing Country Programme for {}".format(country_name)) + def get_cpwbs(wbs): + grp = wbs.split('/') + return '/'.join(grp[:3]) + + results = Result.objects.filter(wbs__isnull=False).exclude(wbs__exact='') + locpwbs = [] + for res in results: + cpwbs = get_cpwbs(res.wbs) + if cpwbs not in locpwbs: + locpwbs.append(cpwbs) + + today = datetime.now() + for i in range(len(locpwbs)): + today = today + timedelta(days=i) + tomorrow = today + timedelta(days=365) + cp, created = CountryProgramme.objects.get_or_create(wbs=locpwbs[i], name='Country Programme '+str(i), from_date=today, to_date=tomorrow) + + time.sleep(5) + + for res in results: + cp = CountryProgramme.objects.get(wbs=get_cpwbs(res.wbs)) + res.country_programme = cp + res.save() + print(res.name) + +def clean_result_types(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + if not country_name: + printtf("country name required /n") + + set_country(country_name) + printtf("Fixing duplicate Result Types for {}".format(country_name)) + fattrs = ["result_set"] + + def relates_to_anything(cobj): + for a in fattrs: + if getattr(cobj, a).count(): + printtf(cobj.id, cobj, "relates to ", a) + return True + return False + + def update_relationships(dpres, keep): + for a in fattrs: + objs = getattr(dpres, a).all() + if len(objs): + for obj in objs: + obj.result_type = keep + obj.save() + printtf("saved obj.id={} obj {} with keepid{} keep {}".format(obj.id, obj, keep.id, keep)) + + def _run_clean(dupes): + printtf(len(dupes), dupes) + for dup in dupes: + dupresults = ResultType.objects.filter(name=dup['name']).all() + delq = [] + keep = None + for dpres in dupresults: + if not keep: + keep = dpres + continue + else: + error = False + if relates_to_anything(dpres): + try: + update_relationships(dpres, keep) + except Exception as exp: + printtf('Cannot remove Object {}, id={}'.format(dpres, dpres.id)) + error = True + if error: + printtf("ERROR OCCURED ON RECORD", dpres.id, dpres) + continue + delq.append(dpres) + if not len(delq): + printtf("Nothing is getting removed for {}".format(dupes)) + else: + # delete everyting in the queue + [i.delete() for i in delq] + printtf("deleting: ", delq) + + # get all duplicates that have the same wbs + dupes = ResultType.objects.values('name').annotate(Count('name')).order_by().filter(name__count__gt=1).all() + _run_clean(dupes) + +def clean_result_structures(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + if not country_name: + printtf("country name required /n") + + set_country(country_name) + printtf("Fixing duplicate Result Structures for {}".format(country_name)) + fattrs = ["result_set", + "indicator_set", + "goal_set", + "pca_set", + "governmentintervention_set", ] + + def relates_to_anything(cobj): + for a in fattrs: + if getattr(cobj, a).count(): + printtf(cobj.id, cobj, "relates to ", a) + return True + return False + + def update_relationships(dpres, keep): + for a in fattrs: + objs = getattr(dpres, a).all() + if len(objs): + for obj in objs: + obj.result_structure = keep + obj.save() + printtf("saved obj.id={} obj {} with keepid{} keep {}".format(obj.id, obj, keep.id, keep)) + + def _run_clean(dupes): + printtf(len(dupes), dupes) + for dup in dupes: + dupresults = ResultStructure.objects.filter(name=dup['name']).all() + delq = [] + keep = None + for dpres in dupresults: + if not keep: + keep = dpres + continue + else: + error = False + if relates_to_anything(dpres): + try: + update_relationships(dpres, keep) + except Exception as exp: + printtf('Cannot remove Object {}, id={}'.format(dpres, dpres.id)) + error = True + if error: + printtf("ERROR OCCURED ON RECORD", dpres.id, dpres) + continue + delq.append(dpres) + if not len(delq): + printtf("Nothing is getting removed for {}".format(dupes)) + else: + # delete everyting in the queue + [i.delete() for i in delq] + printtf("deleting: ", delq) + + # get all duplicates that have the same name + dupes = ResultStructure.objects.values('name', 'from_date', 'to_date').order_by('name', 'from_date', 'to_date').annotate(Count('pk')).filter(pk__count__gt=1).all() + _run_clean(dupes) + + +def delete_all_fcs(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + printtf("Deleting all FCs for {}".format(country_name)) + fcs = FundingCommitment.objects.all() + fcs.delete() + +def dissasociate_result_structures(country_name): + if not country_name: + printtf("country name required /n") + set_country(country_name) + printtf("Dissasociating result structures for {}".format(country_name)) + results = Result.objects.all() + for result in results: + if result.wbs and result.result_structure: + result.result_structure = None + result.save() + + +def all_countries_do(function, name): + for cntry in Country.objects.order_by('name').all(): + if cntry.name in ['Global']: + continue + printtf("CALLING {} for all countries".format(name)) + function(cntry.name) + + + +def before_code_merge(): + # Clean results + all_countries_do(fix_duplicate_results, 'Result Cleaning') + + # Clean results structure + all_countries_do(clean_result_structures, 'Result Structure Cleaning') + + # clean result types + all_countries_do(clean_result_types, 'Result Types Cleaning') + + # Clean indicators + all_countries_do(fix_duplicate_indicators, 'Indicators Cleaning') + + # Delete all fcs + all_countries_do(delete_all_fcs, 'Deleting FCs') + + print('FINISHED WITH BEFORE MERGE') + +def after_code_merge(): #and after migrations + + # set up country programme + all_countries_do(cp_fix, 'Country Programme setup') + + # disassociate result structures + all_countries_do(dissasociate_result_structures, 'Dissasociate Result Structure') + + print("don't forget to sync") diff --git a/EquiTrack/funds/migrations/0004_auto_20160909_2258.py b/EquiTrack/funds/migrations/0004_auto_20160909_2258.py new file mode 100644 index 0000000000..b501802852 --- /dev/null +++ b/EquiTrack/funds/migrations/0004_auto_20160909_2258.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0003_grant_description'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='grant', + unique_together=set([('donor', 'name')]), + ), + ] diff --git a/EquiTrack/funds/migrations/0005_auto_20160910_0836.py b/EquiTrack/funds/migrations/0005_auto_20160910_0836.py new file mode 100644 index 0000000000..1da59e44d2 --- /dev/null +++ b/EquiTrack/funds/migrations/0005_auto_20160910_0836.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0004_auto_20160909_2258'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='grant', + unique_together=set([]), + ), + ] diff --git a/EquiTrack/funds/models.py b/EquiTrack/funds/models.py index 59359e3b19..7c4dfbfa52 100644 --- a/EquiTrack/funds/models.py +++ b/EquiTrack/funds/models.py @@ -19,6 +19,10 @@ def get_queryset(self): return super(GrantManager, self).get_queryset().select_related('donor') +class GrantManager(models.Manager): + def get_queryset(self): + return super(GrantManager, self).get_queryset().select_related('donor') + class Grant(models.Model): donor = models.ForeignKey(Donor) @@ -26,11 +30,11 @@ class Grant(models.Model): description = models.CharField(max_length=255, null=True, blank=True) expiry = models.DateField(null=True, blank=True) + objects = GrantManager() + class Meta: ordering = ['donor'] - objects = GrantManager() - def __unicode__(self): return u"{}: {}".format( self.donor.name, diff --git a/EquiTrack/partners/admin.py b/EquiTrack/partners/admin.py index cc984feae5..2369e7d6af 100644 --- a/EquiTrack/partners/admin.py +++ b/EquiTrack/partners/admin.py @@ -56,7 +56,7 @@ PCAGrantFilter, PCAGatewayTypeFilter, ) -from .mixins import ReadOnlyMixin, SectorMixin +from .mixins import ReadOnlyMixin, SectorMixin, HiddenPartnerMixin from .forms import ( PartnershipForm, PartnersAdminForm, @@ -198,16 +198,15 @@ class LinksInlineAdmin(GenericLinkStackedInline): class IndicatorsInlineAdmin(ReadOnlyMixin, admin.TabularInline): suit_classes = u'suit-tab suit-tab-results' model = RAMIndicator + verbose_name = 'RAM Result' + verbose_name_plural = 'RAM Results' extra = 1 - readonly_fields = ( - u'baseline', - u'target' - ) + fields = ('result',) def formfield_for_foreignkey(self, db_field, request=None, **kwargs): if db_field.name == u'result': - kwargs['queryset'] = Result.objects.filter(result_type__name=u'Output', ram=True) + kwargs['queryset'] = Result.objects.filter(result_type__name=u'Output', ram=True, hidden=False) return super(IndicatorsInlineAdmin, self).formfield_for_foreignkey( db_field, request, **kwargs @@ -260,7 +259,7 @@ def get_readonly_fields(self, request, obj=None): return fields -class PartnershipAdmin(ExportMixin, CountryUsersAdminMixin, VersionAdmin): +class PartnershipAdmin(ExportMixin, CountryUsersAdminMixin, HiddenPartnerMixin, VersionAdmin): form = PartnershipForm resource_class = PCAResource # Add custom exports @@ -483,8 +482,7 @@ class GovernmentInterventionAdmin(admin.ModelAdmin): def formfield_for_foreignkey(self, db_field, request=None, **kwargs): if db_field.rel.to is PartnerOrganization: kwargs['queryset'] = PartnerOrganization.objects.filter( - partner_type=u'Government', - ) + partner_type=u'Government', hidden=False) return super(GovernmentInterventionAdmin, self).formfield_for_foreignkey( db_field, request, **kwargs @@ -713,7 +711,7 @@ class BankDetailsInlineAdmin(admin.StackedInline): extra = 1 -class AgreementAdmin(CountryUsersAdminMixin, admin.ModelAdmin): +class AgreementAdmin(HiddenPartnerMixin, CountryUsersAdminMixin, admin.ModelAdmin): form = AgreementForm list_filter = ( u'partner', @@ -789,13 +787,11 @@ class FundingCommitmentAdmin(admin.ModelAdmin): ) list_filter = ( u'grant', - u'intervention', ) list_display = ( + u'fc_ref', u'grant', - u'intervention', u'fr_number', - u'fc_ref', u'fr_item_amount_usd', u'agreement_amount', u'commitment_amount', diff --git a/EquiTrack/partners/migrations/0067_auto_20160910_0836.py b/EquiTrack/partners/migrations/0067_auto_20160910_0836.py new file mode 100644 index 0000000000..6add8c16bc --- /dev/null +++ b/EquiTrack/partners/migrations/0067_auto_20160910_0836.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0066_auto_20160826_2026'), + ] + + operations = [ + migrations.AlterField( + model_name='fundingcommitment', + name='fc_ref', + field=models.CharField(max_length=50, unique=True, null=True, blank=True), + ), + migrations.AlterField( + model_name='fundingcommitment', + name='grant', + field=models.ForeignKey(blank=True, to='funds.Grant', null=True), + ), + ] diff --git a/EquiTrack/partners/migrations/0068_remove_fundingcommitment_intervention.py b/EquiTrack/partners/migrations/0068_remove_fundingcommitment_intervention.py new file mode 100644 index 0000000000..bae55770f5 --- /dev/null +++ b/EquiTrack/partners/migrations/0068_remove_fundingcommitment_intervention.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0067_auto_20160910_0836'), + ] + + operations = [ + migrations.RemoveField( + model_name='fundingcommitment', + name='intervention', + ), + ] diff --git a/EquiTrack/partners/migrations/0069_auto_20160915_2222.py b/EquiTrack/partners/migrations/0069_auto_20160915_2222.py new file mode 100644 index 0000000000..e2276d1434 --- /dev/null +++ b/EquiTrack/partners/migrations/0069_auto_20160915_2222.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0068_remove_fundingcommitment_intervention'), + ] + + operations = [ + migrations.AlterField( + model_name='governmentintervention', + name='result_structure', + field=models.ForeignKey(to='reports.ResultStructure', on_delete=django.db.models.deletion.DO_NOTHING), + ), + migrations.AlterField( + model_name='pca', + name='result_structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, blank=True, to='reports.ResultStructure', help_text='Which result structure does this partnership report under?', null=True), + ), + ] diff --git a/EquiTrack/partners/migrations/0070_auto_20160915_2340.py b/EquiTrack/partners/migrations/0070_auto_20160915_2340.py new file mode 100644 index 0000000000..84520627ef --- /dev/null +++ b/EquiTrack/partners/migrations/0070_auto_20160915_2340.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import smart_selects.db_fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0069_auto_20160915_2222'), + ] + + operations = [ + migrations.AlterField( + model_name='ramindicator', + name='indicator', + field=smart_selects.db_fields.ChainedForeignKey(chained_model_field=b'result', chained_field=b'result', blank=True, auto_choose=True, to='reports.Indicator', null=True), + ), + ] diff --git a/EquiTrack/partners/mixins.py b/EquiTrack/partners/mixins.py index 563c2c7c24..0a173600a8 100644 --- a/EquiTrack/partners/mixins.py +++ b/EquiTrack/partners/mixins.py @@ -6,7 +6,7 @@ from django.contrib.admin.options import flatten_fieldsets from django.contrib.auth.models import Group -from partners.models import PCASector +from partners.models import PCASector, PartnerOrganization @@ -74,4 +74,15 @@ def get_sector(self, request): def get_pca(self, request): if not getattr(self, '_pca', False): self._pca = self.get_sector_from_request(request).pca - return self._pca \ No newline at end of file + return self._pca + + +class HiddenPartnerMixin(object): + + def formfield_for_foreignkey(self, db_field, request=None, **kwargs): + if db_field.name == u'partner': + kwargs['queryset'] = PartnerOrganization.objects.filter(hidden=False) + + return super(HiddenPartnerMixin, self).formfield_for_foreignkey( + db_field, request, **kwargs + ) \ No newline at end of file diff --git a/EquiTrack/partners/models.py b/EquiTrack/partners/models.py index cae05e4412..def4c1bb5b 100644 --- a/EquiTrack/partners/models.py +++ b/EquiTrack/partners/models.py @@ -746,7 +746,7 @@ class PCA(AdminURLMixin, models.Model): ) result_structure = models.ForeignKey( ResultStructure, - blank=True, null=True, + blank=True, null=True, on_delete=models.DO_NOTHING, help_text=u'Which result structure does this partnership report under?' ) number = models.CharField( @@ -849,7 +849,7 @@ class Meta: def __unicode__(self): return u'{}: {}'.format( self.partner.name, - self.reference_number + self.number if self.number else self.reference_number ) @property @@ -1090,7 +1090,7 @@ class GovernmentIntervention(models.Model): related_name='work_plans', ) result_structure = models.ForeignKey( - ResultStructure, + ResultStructure, on_delete=models.DO_NOTHING ) number = models.CharField( max_length=45L, @@ -1481,6 +1481,8 @@ class RAMIndicator(models.Model): chained_model_field="result", show_all=False, auto_choose=True, + blank=True, + null=True ) @property @@ -1492,8 +1494,7 @@ def target(self): return self.indicator.target def __unicode__(self): - return u'{} -> {} -> {}'.format( - self.result.result_structure.name, + return u'{} -> {}'.format( self.result.sector.name if self.result.sector else '', self.result.__unicode__(), ) @@ -1534,7 +1535,7 @@ def total(self): def __unicode__(self): return u'{} -> {} -> {}'.format( - self.result.result_structure.name, + self.result.result_structure.name if self.result.result_structure else '', self.result.sector.name if self.result.sector else '', self.result.__unicode__(), ) @@ -1640,19 +1641,24 @@ def send_distribution(cls, sender, instance, created, **kwargs): post_save.connect(DistributionPlan.send_distribution, sender=DistributionPlan) +class FCManager(models.Manager): + def get_queryset(self): + return super(FCManager, self).get_queryset().select_related('grant__donor__name') + + class FundingCommitment(TimeFramedModel): - grant = models.ForeignKey(Grant) - intervention = models.ForeignKey(PCA, null=True, related_name='funding_commitments') + grant = models.ForeignKey(Grant, null=True, blank=True) fr_number = models.CharField(max_length=50) wbs = models.CharField(max_length=50) fc_type = models.CharField(max_length=50) - fc_ref = models.CharField(max_length=50, blank=True, null=True) + fc_ref = models.CharField(max_length=50, blank=True, null=True, unique=True) fr_item_amount_usd = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True) agreement_amount = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True) commitment_amount = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True) expenditure_amount = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True) + objects = FCManager() class DirectCashTransfer(models.Model): diff --git a/EquiTrack/partners/templatetags/intervention_tags.py b/EquiTrack/partners/templatetags/intervention_tags.py index ed92f46da0..77f9de3f1e 100644 --- a/EquiTrack/partners/templatetags/intervention_tags.py +++ b/EquiTrack/partners/templatetags/intervention_tags.py @@ -108,7 +108,7 @@ def show_fr_fc(value): return '' intervention = PCA.objects.get(id=int(value)) - commitments = FundingCommitment.objects.filter(intervention=intervention) + commitments = FundingCommitment.objects.filter(fr_number=intervention.fr_number) data = tablib.Dataset() fr_fc_summary = [] diff --git a/EquiTrack/partners/tests/test_models.py b/EquiTrack/partners/tests/test_models.py index 3fa83df855..6b35fe6439 100644 --- a/EquiTrack/partners/tests/test_models.py +++ b/EquiTrack/partners/tests/test_models.py @@ -130,7 +130,6 @@ def setUp(self): start=current_cp.from_date, end=current_cp.from_date+datetime.timedelta(days=200), grant=grant, - intervention=self.intervention, fr_number='0123456789', wbs='Test', fc_type='PCA', @@ -140,7 +139,6 @@ def setUp(self): start=current_cp.from_date+datetime.timedelta(days=200), end=current_cp.to_date, grant=grant, - intervention=self.intervention, fr_number='0123456789', wbs='Test', fc_type='PCA', @@ -152,10 +150,11 @@ def test_planned_cash_transfers(self): total = self.intervention.partner.planned_cash_transfers self.assertEqual(total, 60000) - def test_actual_cash_transferred(self): - total = self.intervention.partner.actual_cash_transferred - self.assertEqual(total, 40000) + # The total cash transferred and actual cash transferred for the partner now comes from vision + # def test_actual_cash_transferred(self): + # total = self.intervention.partner.actual_cash_transferred + # self.assertEqual(total, 40000) - def test_total_cash_transferred(self): - total = self.intervention.partner.total_cash_transferred - self.assertEqual(total, 80000) \ No newline at end of file + # def test_total_cash_transferred(self): + # total = self.intervention.partner.total_cash_transferred + # self.assertEqual(total, 80000) diff --git a/EquiTrack/reports/admin.py b/EquiTrack/reports/admin.py index 7d12b2e7c8..24cd2c8025 100644 --- a/EquiTrack/reports/admin.py +++ b/EquiTrack/reports/admin.py @@ -16,6 +16,7 @@ ResultStructure, ResultType, Result, + CountryProgramme ) from .forms import IndicatorAdminForm @@ -57,12 +58,6 @@ class SectorAdmin(admin.ModelAdmin): list_editable = ('color', 'dashboard',) -class ResultStructureAdmin(admin.ModelAdmin): - search_fields = ('name',) - list_filter = ('result_structure', SectorListFilter,) - list_display = ('name', 'sector', 'result_structure',) - - class GoalAdmin(admin.ModelAdmin): form = AutoSizeTextForm @@ -111,7 +106,7 @@ class GoalAdmin(admin.ModelAdmin): class IndicatorAdmin(admin.ModelAdmin): form = IndicatorAdminForm - search_fields = ('name',) + search_fields = ('name','code') list_editable = ( 'view_on_dashboard', ) @@ -163,7 +158,7 @@ class ResultAdmin(MPTTModelAdmin): 'name', ) list_filter = ( - 'result_structure', + 'country_programme', 'sector', 'result_type', HiddenResultFilter, @@ -201,6 +196,7 @@ def show_results(self, request, queryset): admin.site.register(Result, ResultAdmin) admin.site.register(ResultStructure) +admin.site.register(CountryProgramme) admin.site.register(Sector, SectorAdmin) admin.site.register(Goal, GoalAdmin) admin.site.register(Unit, ImportExportModelAdmin) diff --git a/EquiTrack/reports/fixtures/initial_data.json b/EquiTrack/reports/fixtures/initial_data.json index bafcbc86c7..0be00b32f0 100644 --- a/EquiTrack/reports/fixtures/initial_data.json +++ b/EquiTrack/reports/fixtures/initial_data.json @@ -1,17 +1,22 @@ [ - { - "pk": 1, - "model": "reports.resulttype", - "fields": {"name": "Outcome"} - }, - { - "pk": 2, - "model": "reports.resulttype", - "fields": {"name": "Output"} - }, - { - "pk": 3, - "model": "reports.resulttype", - "fields": {"name": "Activity"} - } -] \ No newline at end of file + { + "pk": 1, + "model": "reports.resulttype", + "fields": {"name": "Outcome"} + }, + { + "pk": 2, + "model": "reports.resulttype", + "fields": {"name": "Output"} + }, + { + "pk": 3, + "model": "reports.resulttype", + "fields": {"name": "Activity"} + }, + { + "pk": 4, + "model": "reports.resulttype", + "fields": {"name": "Sub-activity"} + } +] diff --git a/EquiTrack/reports/migrations/0020_indicator_assumptions.py b/EquiTrack/reports/migrations/0020_indicator_assumptions.py new file mode 100644 index 0000000000..54d5f69a65 --- /dev/null +++ b/EquiTrack/reports/migrations/0020_indicator_assumptions.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0019_auto_20160825_1857'), + ] + + operations = [ + migrations.AddField( + model_name='indicator', + name='assumptions', + field=models.TextField(null=True, blank=True), + ), + ] diff --git a/EquiTrack/reports/migrations/0021_auto_20160906_1925.py b/EquiTrack/reports/migrations/0021_auto_20160906_1925.py new file mode 100644 index 0000000000..dfb82f76bc --- /dev/null +++ b/EquiTrack/reports/migrations/0021_auto_20160906_1925.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0020_indicator_assumptions'), + ] + + operations = [ + migrations.CreateModel( + name='CountryProgramme', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=150)), + ('wbs', models.CharField(unique=True, max_length=30)), + ('from_date', models.DateField()), + ('to_date', models.DateField()), + ], + ), + migrations.AlterField( + model_name='result', + name='result_structure', + field=models.ForeignKey(blank=True, to='reports.ResultStructure', null=True), + ), + migrations.AlterField( + model_name='resulttype', + name='name', + field=models.CharField(unique=True, max_length=150, choices=[(b'Outcome', b'Outcome'), (b'Output', b'Output'), (b'Activity', b'Activity'), (b'Sub-Activity', b'Sub-Activity')]), + ), + migrations.AlterUniqueTogether( + name='indicator', + unique_together=set([]), + ), + migrations.AddField( + model_name='result', + name='country_programme', + field=models.ForeignKey(blank=True, to='reports.CountryProgramme', null=True), + ), + migrations.AddField( + model_name='resultstructure', + name='country_programme', + field=models.ForeignKey(blank=True, to='reports.CountryProgramme', null=True), + ), + ] diff --git a/EquiTrack/reports/migrations/0022_auto_20160906_1927.py b/EquiTrack/reports/migrations/0022_auto_20160906_1927.py new file mode 100644 index 0000000000..c98f773a54 --- /dev/null +++ b/EquiTrack/reports/migrations/0022_auto_20160906_1927.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0021_auto_20160906_1925'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='indicator', + unique_together=set([('name', 'result', 'sector')]), + ), + migrations.AlterUniqueTogether( + name='result', + unique_together=set([('wbs', 'country_programme')]), + ), + migrations.AlterUniqueTogether( + name='resultstructure', + unique_together=set([('name', 'from_date', 'to_date')]), + ), + ] diff --git a/EquiTrack/reports/migrations/0023_auto_20160909_2203.py b/EquiTrack/reports/migrations/0023_auto_20160909_2203.py new file mode 100644 index 0000000000..9e61c86ffa --- /dev/null +++ b/EquiTrack/reports/migrations/0023_auto_20160909_2203.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0022_auto_20160906_1927'), + ] + + operations = [ + migrations.AlterField( + model_name='indicator', + name='activity_info_indicators', + field=models.ManyToManyField(to='activityinfo.Indicator', blank=True), + ), + ] diff --git a/EquiTrack/reports/migrations/0024_auto_20160915_2222.py b/EquiTrack/reports/migrations/0024_auto_20160915_2222.py new file mode 100644 index 0000000000..de9c579d4b --- /dev/null +++ b/EquiTrack/reports/migrations/0024_auto_20160915_2222.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0023_auto_20160909_2203'), + ] + + operations = [ + migrations.AlterField( + model_name='goal', + name='result_structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, blank=True, to='reports.ResultStructure', null=True), + ), + migrations.AlterField( + model_name='indicator', + name='result_structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, blank=True, to='reports.ResultStructure', null=True), + ), + migrations.AlterField( + model_name='result', + name='result_structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, blank=True, to='reports.ResultStructure', null=True), + ), + ] diff --git a/EquiTrack/reports/models.py b/EquiTrack/reports/models.py index bdceb07ad7..d780473dae 100644 --- a/EquiTrack/reports/models.py +++ b/EquiTrack/reports/models.py @@ -1,5 +1,4 @@ -__author__ = 'jcranwellward' - +from datetime import datetime from django.db import models import django.contrib.postgres.fields as pgfields from jsonfield import JSONField @@ -15,15 +14,34 @@ from django.utils.functional import cached_property -# TODO: move to the global schema + +class CountryProgramme(models.Model): + name = models.CharField(max_length=150) + wbs = models.CharField(max_length=30, unique=True) + from_date = models.DateField() + to_date = models.DateField() + + + def __unicode__(self): + return ' '.join([self.name, self.wbs]) + + @classmethod + def current(cls): + today = datetime.now() + return cls.objects.get(to_date__gte=today, from_date__lt=today) + + class ResultStructure(models.Model): name = models.CharField(max_length=150) + country_programme = models.ForeignKey(CountryProgramme, null=True, blank=True) from_date = models.DateField() to_date = models.DateField() + # TODO: add validation these dates should never extend beyond the country programme structure class Meta: ordering = ['name'] + unique_together = (("name", "from_date", "to_date"),) def __unicode__(self): return self.name @@ -34,8 +52,18 @@ def current(cls): class ResultType(models.Model): - - name = models.CharField(max_length=150) + OUTCOME = 'Outcome' + OUTPUT = 'Output' + ACTIVITY = 'Activity' + SUBACTIVITY = 'Sub-Activity' + + NAME_CHOICES = ( + (OUTCOME, 'Outcome'), + (OUTPUT, 'Output'), + (ACTIVITY, 'Activity'), + (SUBACTIVITY, 'Sub-Activity'), + ) + name = models.CharField(max_length=150, unique=True, choices=NAME_CHOICES) def __unicode__(self): return self.name @@ -74,12 +102,13 @@ def __unicode__(self): class ResultManager(models.Manager): def get_queryset(self): - return super(ResultManager, self).get_queryset().select_related('result_structure', 'result_type') + return super(ResultManager, self).get_queryset().select_related('country_programme', 'result_structure', 'result_type') class Result(MPTTModel): - result_structure = models.ForeignKey(ResultStructure) + result_structure = models.ForeignKey(ResultStructure, null=True, blank=True, on_delete=models.DO_NOTHING) + country_programme = models.ForeignKey(CountryProgramme, null=True, blank=True) result_type = models.ForeignKey(ResultType) sector = models.ForeignKey(Sector, null=True, blank=True) name = models.TextField() @@ -126,6 +155,7 @@ class Result(MPTTModel): class Meta: ordering = ['name'] + unique_together = (('wbs', 'country_programme'),) @cached_property def result_name(self): @@ -142,8 +172,14 @@ def __unicode__(self): self.name ) - def save(self, *args, **kwargs): + def valid_entry(self): + if self.wbs: + return self.wbs.startswith(self.country_programme.wbs) + def save(self, *args, **kwargs): + # TODO add a validator that makes sure that the current result wbs fits within the countryProgramme wbs + if not self.wbs: + self.wbs = None super(Result, self).save(*args, **kwargs) nodes = self.get_descendants() for node in nodes: @@ -162,7 +198,7 @@ class Milestone(models.Model): class Goal(models.Model): result_structure = models.ForeignKey( - ResultStructure, blank=True, null=True) + ResultStructure, blank=True, null=True, on_delete=models.DO_NOTHING) sector = models.ForeignKey(Sector, related_name='goals') name = models.CharField(max_length=512L, unique=True) description = models.CharField(max_length=512L, blank=True) @@ -193,7 +229,7 @@ class Indicator(models.Model): ) result_structure = models.ForeignKey( ResultStructure, - blank=True, null=True + blank=True, null=True, on_delete=models.DO_NOTHING ) result = models.ForeignKey(Result, null=True, blank=True) @@ -205,6 +241,7 @@ class Indicator(models.Model): sector_total = models.IntegerField(verbose_name='Sector Target', null=True, blank=True) current = models.IntegerField(null=True, blank=True, default=0) sector_current = models.IntegerField(null=True, blank=True) + assumptions = models.TextField(null=True, blank=True) # RAM Info target = models.CharField(max_length=255, null=True, blank=True) @@ -216,6 +253,7 @@ class Indicator(models.Model): in_activity_info = models.BooleanField(default=False) activity_info_indicators = models.ManyToManyField( 'activityinfo.Indicator', + blank=True ) class Meta: @@ -254,3 +292,9 @@ def progress(self, result_structure=None): ) total = programmed.aggregate(models.Sum('current_progress')) return (total[total.keys()[0]] or 0) + self.current if self.current else 0 + + def save(self, *args, **kwargs): + # Prevent from saving empty strings as code because of the unique together constraint + if not self.code: + self.code = None + super(Indicator, self).save(*args, **kwargs) diff --git a/EquiTrack/reports/tests/test_views.py b/EquiTrack/reports/tests/test_views.py index 4900490043..2f722ad729 100644 --- a/EquiTrack/reports/tests/test_views.py +++ b/EquiTrack/reports/tests/test_views.py @@ -1,6 +1,7 @@ __author__ = 'achamseddine' import random +import factory from rest_framework import status @@ -28,6 +29,10 @@ def setUp(self): self.milestone1 = MilestoneFactory(result=self.result1) self.milestone2 = MilestoneFactory(result=self.result1) + # Additional data to use in tests + self.location3 = LocationFactory() + self.section3 = SectionFactory() + def test_api_resultstructures_list(self): response = self.forced_auth_req('get', '/api/reports/result-structures/', user=self.unicef_staff) @@ -50,8 +55,26 @@ def test_api_indicators_list(self): def test_api_results_list(self): response = self.forced_auth_req('get', '/api/reports/results/', user=self.unicef_staff) + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(int(response.data[0]["id"]), self.result1.id) + def test_api_results_patch(self): + url = '/api/reports/results/{}/'.format(self.result1.id) + data = {"name": "patched name"} + response = self.forced_auth_req('patch', url, user=self.unicef_staff, data=data) + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data["name"], "patched name") + + def test_api_results_update_m2m(self): + url = '/api/reports/results/{}/'.format(self.result1.id) + data = { + "geotag": [self.location1.id, self.location3.id], + "sections": [self.section1.id, self.section3.id] + } + response = self.forced_auth_req('patch', url, user=self.unicef_staff, data=data) self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertItemsEqual(response.data["geotag"], (self.location1.id, self.location3.id,)) + self.assertItemsEqual(response.data["sections"], [self.section1.id, self.section3.id]) def test_api_units_list(self): response = self.forced_auth_req('get', '/api/reports/units/', user=self.unicef_staff) diff --git a/EquiTrack/reports/views.py b/EquiTrack/reports/views.py index cb76a4d00f..623da33b94 100644 --- a/EquiTrack/reports/views.py +++ b/EquiTrack/reports/views.py @@ -71,8 +71,7 @@ class MilestoneViewSet(viewsets.ModelViewSet): permission_classes = (IsAdminUser,) -class ResultViewSet(mixins.ListModelMixin, - viewsets.GenericViewSet): +class ResultViewSet(viewsets.ModelViewSet): """ Returns a list of all Results """ @@ -81,13 +80,13 @@ class ResultViewSet(mixins.ListModelMixin, permission_classes = (IsAdminUser,) -class IndicatorViewSet(mixins.ListModelMixin, - viewsets.GenericViewSet): +class IndicatorViewSet(viewsets.ModelViewSet): """ - Returns a list of all Indicators + CRUD api for Indicators """ queryset = Indicator.objects.all() serializer_class = IndicatorCreateSerializer + permission_classes = (IsAdminUser,) class UnitViewSet(mixins.RetrieveModelMixin, diff --git a/EquiTrack/templates/base.html b/EquiTrack/templates/base.html index 5b714139a6..cbcb7aea3d 100644 --- a/EquiTrack/templates/base.html +++ b/EquiTrack/templates/base.html @@ -157,9 +157,9 @@