diff --git a/.gitignore b/.gitignore index aa11eb4cea..448fe49378 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ wheelhouse # Frontend .frontend/ +.fe/ .tmp .editorconfig Icon diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..cbf06168b6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# eTools and GitHub Best Practices - Contributing Guide + +##Concepts Overview + +###GitFlow + +[GitFlow](https://datasift.github.io/gitflow/IntroducingGitFlow.html) is a branching model for git designed to allow teams to collaborate and continuously develop new features while keeping finished work separate. + +It involves 5 major components within the eTools project: + + * Master branch + * Develop branch + * Feature branches + * Staging branch + * Hotfix branches + +Feature branches are where the bulk of the work is done such as creating new features. Developers branch off of the develop branch, work on their feature(s) and submit a pull request to merge the feature back into the develop branch when it is complete. This is also where *continuous integration* is used. + +Approximately every 2 weeks (every sprint) the project is merged into the staging branch. This is where the new features and fixes are QA tested. As QA finds problems and fixes are made, this branch is frequently merged back into develop so the fixes are not lost as new development continues off of that branch. + +Every release, the staging branch is merged into master and tagged with the release version number. The master branch is the released product; only finished and vigorously tested code is put here and made available to consumers. + +Hotfix branches are used for emergency fixes for problems found after a major release to master. Hotfixes are branched directly off master; the fix is made and then merged back into master (while also being tagged) and into staging and develop (to make sure developers are working with this fix so it doesn’t pop up again in a new release). + +###Continuous Integration + +[Continuous integration (CI)](https://www.thoughtworks.com/continuous-integration) is a development practice requiring developers to very frequently integrate code into the develop branch. + +Each integration is built and tested by an automated CI server. If there are any issues with the build, the team is notified and solves them before any new commits are made. + +The idea is to avoid situations involving lots of independent code developed over a long period of time being integrated together only to find many problems that require lots of backtracking to find and fix. Continuous integration methodology means problems are often identified and solved right away since they involve code which was just recently worked on. + +After a pull request has been approved and the project builds successfully, it is automatically deployed to a production environment. This is called *continuous deployment* and it allows the team quickly move towards working software and view the eTools develop stage app in an actual production environment. + +##Practices and Rituals + +###Commiting + +The following guidelines should be followed when writing commit messages to ensure readability when viewing project history. The formatting can be added conventionally in git/GitHub or through the use of a CLI wizard ([Commitizen](https://github.com/commitizen/cz-cli)). To use the wizard, run `npm run commit` in your terminal after staging your changes in git. + +Each commit message consists of a **header**, a **body** and a **footer**. + +A typical commit message will look something like this... ![ExampleCommit](http://i.imgur.com/9SwquPt.png) +* `fix(copy): fix handling of typed subarrays` is the **header**. Each header consists of a **type**, a **scope** and a **subject**: + * `fix` is the **type**. The type is picked from a limited set of words that categorize the change. Must be one of the following: + * **feat**: A new feature + * **fix**: A bug fix + * **docs**: Documentation only changes + * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing + semi-colons, etc) + * **refactor**: A code change that neither fixes a bug nor adds a feature + * **perf**: A code change that improves performance + * **test**: Adding missing tests + * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation + generation + * `(copy)` is the **scope**, an optional field that specifies where in the application the change was made. + * `fix handling of typed subarrays` is the **subject**, a brief description of the change. + * use present tense: "change" + * don't capitalize the first letter + * no period at the end +* ``Previously, it would return a copy of the whole original typed array, not its slice. Now, the `byteOffset` and `length` are also preserved.`` This is the **body**. The body should include the motivation for the change and contrast this with previous behavior. Once again present tense is used. +* `Fixes #14842` and `Closes #14845` are both part of the **footer**. The footer is the place to +reference any GitHub issues or pull requests that this commit fixes/closes. + +Any line of the commit message (including the header) should be no longer than 100 characters. + +###Reverting a Commit + +If the commit reverts a previous commit, the **type** should be `revert: ` followed by the entire header of the reverted commit in quotes. So to revert the example above you would write `revert: "fix(copy): fix handling of typed subarrays"` +In the body it should say: `This reverts commit .` where the hash is the SHA of the commit being reverted. + +###Tagging + +[Tagging](https://git-scm.com/book/en/v2/Git-Basics-Tagging) is a process that happens whenever a new release (or a hotfix) is merged with master. The release is tagged with a version number like `v1.2.0` + +Tagging is also used on commits to label their association with a certain version of the application. Searching for that tag then gets a list of all the commits that were tagged as part of that version. + +###Issues and Labels + +The eTools team uses [Pivotal Tracker](https://www.pivotaltracker.com/) for issue tracking. It’s a way for team members to keep track of not only bugs but new feature ideas, optimizations, and more general “to-dos” and have the rest of the team discuss these issues. + +Issues often have a milestone associated with them, like a project version or a specific time period to which the issue is relevant. There’s also a section for assignee's, who have been tasked with fixing that specific issue. Finally there are labels, which act as a way to organize issues, make them easily searchable, and give people a quick idea of what the issue involves. + +###Issues in GitHub + +Since not all developers will have access to eTools' private Pivotal Tracker dashboard, we recommend that any issues identified by developers without access to Pivotal Tracker will be raised in GitHub prior to fixing them in the code and making a Pull Request. + +###Labels in GitHub + +Labels in GitHub are used in a color coded system. The color is a universal identifier for the team. It could specify a platform that the issue is relevant to, a problem in production, a desire for feedback or improvements, or a new addition. The actual text on the label is more specific. A *platform* label could have the text “python”, and now someone looking at the label knows the issue resides on the Python back-end of the app. Or maybe an *addition* label will have “feature” in the text, so this issue involves a brand new feature that has yet to be added. + +This diagram outlines the labeling methodology + +![LabelGuide](http://i.imgur.com/dWfLNeS.png) + +###Sources + +https://datasift.github.io/gitflow/IntroducingGitFlow.html +https://www.thoughtworks.com/continuous-integration +https://git-scm.com/book/en/v2/Git-Basics-Tagging +https://guides.github.com/features/issues/ +https://robinpowered.com/blog/best-practice-system-for-organizing-and-tagging-github-issues/ +https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md diff --git a/EquiTrack/EquiTrack/factories.py b/EquiTrack/EquiTrack/factories.py index 6c0193d7d9..7168adb9a8 100644 --- a/EquiTrack/EquiTrack/factories.py +++ b/EquiTrack/EquiTrack/factories.py @@ -3,7 +3,7 @@ """ __author__ = 'jcranwellward' -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from django.db.models.signals import post_save import factory @@ -126,7 +126,6 @@ class Meta: model = partner_models.PartnerOrganization name = factory.Sequence(lambda n: 'Partner {}'.format(n)) - staff = factory.RelatedFactory(PartnerStaffFactory, 'partner') @@ -138,6 +137,7 @@ class Meta: agreement_type = u'PCA' + class PartnershipFactory(factory.django.DjangoModelFactory): class Meta: model = partner_models.PCA @@ -149,6 +149,15 @@ class Meta: initiation_date = datetime.today() +class ResultStructureFactory(factory.django.DjangoModelFactory): + class Meta: + model = report_models.ResultStructure + + name = factory.Sequence(lambda n: 'RSSP {}'.format(n)) + from_date = date(date.today().year, 1, 1) + to_date = date(date.today().year, 12, 31) + + # class FundingCommitmentFactory(factory.django.DjangoModelFactory): # class Meta: # model = partner_models.FundingCommitment diff --git a/EquiTrack/EquiTrack/views.py b/EquiTrack/EquiTrack/views.py index ff719c12be..373f8de82e 100644 --- a/EquiTrack/EquiTrack/views.py +++ b/EquiTrack/EquiTrack/views.py @@ -219,9 +219,9 @@ class HACTDashboardView(TemplateView): def get_context_data(self, **kwargs): return { 'partners': PartnerOrganization.objects.filter( - documents__status__in=[ + Q(documents__status__in=[ PCA.ACTIVE, PCA.IMPLEMENTED - ] + ]) | (Q(partner_type=u'Government') & Q(work_plans__isnull=False)) ).distinct() } diff --git a/EquiTrack/assets/css/main.css b/EquiTrack/assets/css/main.css index 3654a30062..9b60d15528 100644 --- a/EquiTrack/assets/css/main.css +++ b/EquiTrack/assets/css/main.css @@ -76,11 +76,10 @@ img { } #header .brand { float: left; - width: 280px; + width: auto; min-height: 60px; padding: 0 0 0 10px; position: relative; - background: #0099ff; } #header .logo { @@ -91,6 +90,11 @@ img { display: inline-block; } +#header .logo #test-text { + display: none; + color: #e6e600; +} + #header .logoimg { margin: 0px 0px 10px 5px; } diff --git a/EquiTrack/funds/models.py b/EquiTrack/funds/models.py index 59ab4fdd9e..59359e3b19 100644 --- a/EquiTrack/funds/models.py +++ b/EquiTrack/funds/models.py @@ -13,6 +13,12 @@ def __unicode__(self): return self.name +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) @@ -23,6 +29,8 @@ class Grant(models.Model): class Meta: ordering = ['donor'] + objects = GrantManager() + def __unicode__(self): return u"{}: {}".format( self.donor.name, diff --git a/EquiTrack/locations/models.py b/EquiTrack/locations/models.py index 18282ad75f..808c78bc1e 100644 --- a/EquiTrack/locations/models.py +++ b/EquiTrack/locations/models.py @@ -101,6 +101,12 @@ class Meta: ordering = ['name'] +class LocationManager(models.Manager): + + def get_queryset(self): + return super(LocationManager, self).get_queryset().select_related('gateway', 'locality') + + class Location(MPTTModel): name = models.CharField(max_length=254L) @@ -115,6 +121,8 @@ class Location(MPTTModel): point = models.PointField(null=True, blank=True) objects = models.GeoManager() + objects = LocationManager() + def __unicode__(self): #TODO: Make generic return u'{} ({} {})'.format( diff --git a/EquiTrack/partners/admin.py b/EquiTrack/partners/admin.py index c3c6fa467b..214132f6a5 100644 --- a/EquiTrack/partners/admin.py +++ b/EquiTrack/partners/admin.py @@ -475,9 +475,10 @@ class GovernmentInterventionAdmin(admin.ModelAdmin): ) inlines = [GovernmentInterventionResultAdminInline] - suit_form_includes = ( - ('admin/partners/government_funding.html', 'bottom'), - ) + # government funding disabled temporarily. awaiting Vision API updates + # suit_form_includes = ( + # ('admin/partners/government_funding.html', 'bottom'), + # ) def formfield_for_foreignkey(self, db_field, request=None, **kwargs): if db_field.rel.to is PartnerOrganization: diff --git a/EquiTrack/partners/forms.py b/EquiTrack/partners/forms.py index 02a9e23bee..16b9cfaf1d 100644 --- a/EquiTrack/partners/forms.py +++ b/EquiTrack/partners/forms.py @@ -291,6 +291,11 @@ def clean(self): class AgreementForm(UserGroupForm): + ERROR_MESSAGES = { + 'end_date': 'End date must be greater than start date', + 'start_date_val': 'Start date must be greater than laatest of signed by partner/unicef date', + } + user_field = u'signed_by' group_name = u'Senior Management Team' @@ -310,14 +315,19 @@ def clean(self): agreement_number = cleaned_data.get(u'agreement_number') start = cleaned_data.get(u'start') end = cleaned_data.get(u'end') + signed_by_partner_date = cleaned_data.get(u'signed_by_partner_date') + signed_by_unicef_date = cleaned_data.get(u'signed_by_unicef_date') if partner and agreement_type == Agreement.PCA: # Partner can only have one active PCA # pca_ids = partner.agreement_set.filter(agreement_type=Agreement.PCA).values_list('id', flat=True) # if (not self.instance.id and pca_ids) or \ # (self.instance.id and pca_ids and self.instance.id not in pca_ids): - if partner.get_last_agreement and partner.get_last_agreement != self.instance: - if not start > partner.get_last_agreement.start and not end > partner.get_last_agreement.end: + if start and end and \ + partner.get_last_pca and \ + partner.get_last_pca != self.instance: + + if start < partner.get_last_pca.end: err = u'This partner can only have one active {} agreement'.format(agreement_type) raise ValidationError({'agreement_type': err}) @@ -341,6 +351,37 @@ def clean(self): _(u'SSFA can not be more than a year') ) + if start and end and start > end: + raise ValidationError({'end': self.ERROR_MESSAGES['end_date']}) + + # check if start date is greater than or equal than greatest signed date + if signed_by_partner_date and signed_by_unicef_date and start: + if signed_by_partner_date > signed_by_unicef_date: + if start < signed_by_partner_date: + raise ValidationError({'start': self.ERROR_MESSAGES['start_date_val']}) + else: + if start < signed_by_unicef_date: + raise ValidationError({'start': self.ERROR_MESSAGES['start_date_val']}) + + if self.instance.id and self.instance.agreement_type != agreement_type \ + and signed_by_partner_date and signed_by_unicef_date: + raise ValidationError( + _(u'Agreement type can not be changed once signed by unicef and partner ') + ) + + # set start date to one of the signed dates + if start is None and agreement_type == Agreement.PCA: + # if both signed dates exist + if signed_by_partner_date and signed_by_unicef_date: + if signed_by_partner_date > signed_by_unicef_date: + self.cleaned_data[u'start'] = signed_by_partner_date + else: + self.cleaned_data[u'start'] = signed_by_unicef_date + + # set end date to result structure end date + if end is None: + self.cleaned_data[u'end'] = ResultStructure.current().to_date + # TODO: prevent more than one agreement being created for the current period # agreements = Agreement.objects.filter( # partner=partner, @@ -573,7 +614,7 @@ def clean(self): signed_by_partner_date = cleaned_data[u'signed_by_partner_date'] start_date = cleaned_data[u'start_date'] end_date = cleaned_data[u'end_date'] - initiation_date = cleaned_data[u'initiation_date'] + initiation_date = cleaned_data.get(u'initiation_date') submission_date = cleaned_data[u'submission_date'] review_date = cleaned_data[u'review_date'] diff --git a/EquiTrack/partners/migrations/0060_auto_20160721_2313.py b/EquiTrack/partners/migrations/0060_auto_20160721_2313.py new file mode 100644 index 0000000000..f506525a20 --- /dev/null +++ b/EquiTrack/partners/migrations/0060_auto_20160721_2313.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0059_auto_20160621_2228'), + ] + + operations = [ + migrations.AlterField( + model_name='pca', + name='number', + field=models.CharField(max_length=45L, null=True, verbose_name='Reference Number', blank=True), + ), + ] diff --git a/EquiTrack/partners/models.py b/EquiTrack/partners/models.py index 61b4aa1279..1fd496c79c 100644 --- a/EquiTrack/partners/models.py +++ b/EquiTrack/partners/models.py @@ -7,11 +7,12 @@ from django.db.models import Q from django.conf import settings -from django.db import models, connection +from django.db import models, connection, transaction from django.contrib.auth.models import Group from django.db.models.signals import post_save, pre_delete from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from django.utils.functional import cached_property from jsonfield import JSONField from django_hstore import hstore @@ -22,6 +23,7 @@ ) from model_utils import Choices + from EquiTrack.utils import get_changeform_link from EquiTrack.mixins import AdminURLMixin @@ -156,6 +158,8 @@ class PartnerOrganization(AdminURLMixin, models.Model): ) vision_synced = models.BooleanField(default=False) + + class Meta: ordering = ['name'] unique_together = ('name', 'vendor_number') @@ -166,10 +170,14 @@ def __unicode__(self): def latest_assessment(self, type): return self.assessments.filter(type=type).order_by('completed_date').last() - @property - def get_last_agreement(self): - return Agreement.objects.filter( - partner=self + @cached_property + def get_last_pca(self): + # exclude Agreements that were not signed + return self.agreement_set.filter( + agreement_type=Agreement.PCA + ).exclude( + signed_by_unicef_date__isnull=True, + signed_by_partner_date__isnull=True ).order_by('signed_by_unicef_date').last() @property @@ -240,13 +248,22 @@ def planned_cash_transfers(self): Planned cash transfers for the current year """ year = datetime.date.today().year - total = PartnershipBudget.objects.filter( - partnership__partner=self, - partnership__status__in=[PCA.ACTIVE, PCA.IMPLEMENTED], - year=year).aggregate( - models.Sum('unicef_cash') - ) - return total[total.keys()[0]] or 0 + if self.partner_type == u'Government': + total = GovernmentInterventionResult.objects.filter( + intervention__partner=self, + year=year).aggregate( + models.Sum('planned_amount') + )['planned_amount__sum'] or 0 + else: + q = PartnershipBudget.objects.filter(partnership__partner=self, + partnership__status__in=[PCA.ACTIVE, + PCA.IMPLEMENTED], + year=year) + q = q.order_by("partnership__id", "-created").\ + distinct('partnership__id').values_list('unicef_cash', flat=True) + total = sum(q) + + return total @property def actual_cash_transferred(self): @@ -274,20 +291,49 @@ def total_cash_transferred(self): total = FundingCommitment.objects.filter( end__gte=cp.from_date, end__lte=cp.to_date, + # this or intervention__partner=self, intervention__status__in=[PCA.ACTIVE, PCA.IMPLEMENTED]).aggregate( models.Sum('expenditure_amount') + # government_intervention in + # gov_intervention__partner=self, + # gov_intervention__status__in=[GovernmentIntervention.ACTIVE, GovernmentIntervention.IMPLEMENTED] + # ).aggregate( + # models.Sum('expenditure_amount') ) return total[total.keys()[0]] or 0 @property def planned_visits(self): - planned = self.documents.filter( - status__in=[PCA.ACTIVE, PCA.IMPLEMENTED] - ).aggregate( - models.Sum('planned_visits') - ) - return planned[planned.keys()[0]] or 0 + from trips.models import Trip + # planned visits + pv = 0 + + + pv = self.cp_cycle_trip_links.filter( + trip__travel_type=Trip.PROGRAMME_MONITORING + ).exclude( + trip__status__in=[Trip.CANCELLED, Trip.COMPLETED] + ).count() or 0 + + + return pv + + @cached_property + def cp_cycle_trip_links(self): + from trips.models import Trip + crs = ResultStructure.current() + if self.partner_type == u'Government': + return self.linkedgovernmentpartner_set.filter( + trip__from_date__lt=crs.to_date, + trip__from_date__gte=crs.from_date + ).distinct('trip') + else: + return self.linkedpartner_set.filter( + trip__from_date__lt=crs.to_date, + trip__from_date__gte=crs.from_date + ).distinct('trip') + @property def trips(self): @@ -305,13 +351,22 @@ def trips(self): @property def programmatic_visits(self): + ''' + :return: all done programmatic visits + ''' from trips.models import Trip - return self.trips.filter(travel_type=Trip.PROGRAMME_MONITORING).count() + return self.cp_cycle_trip_links.filter( + trip__travel_type=Trip.PROGRAMME_MONITORING, + trip__status__in=[Trip.COMPLETED] + ).count() @property def spot_checks(self): from trips.models import Trip - return self.trips.filter(travel_type=Trip.SPOT_CHECK).count() + return self.cp_cycle_trip_links.filter( + trip__travel_type=Trip.SPOT_CHECK, + trip__status__in=[Trip.COMPLETED] + ).count() @property def follow_up_flags(self): @@ -1034,6 +1089,11 @@ class GovernmentIntervention(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + def __unicode__(self): + return u'Number: {}'.format(self.number) if self.number else \ + u'{}: {}'.format(self.pk, + self.reference_number) + #country/partner/year/# @property def reference_number(self): @@ -1061,6 +1121,7 @@ def save(self, **kwargs): super(GovernmentIntervention, self).save(**kwargs) + class GovernmentInterventionResult(models.Model): intervention = models.ForeignKey( @@ -1102,6 +1163,7 @@ class GovernmentInterventionResult(models.Model): objects = hstore.HStoreManager() + @transaction.atomic def save(self, **kwargs): super(GovernmentInterventionResult, self).save(**kwargs) @@ -1128,11 +1190,16 @@ def save(self, **kwargs): if ref_activity.code not in self.activities: ref_activity.delete() + @transaction.atomic def delete(self, using=None): self.activities_list.all().delete() super(GovernmentInterventionResult, self).delete(using=using) + def __unicode__(self): + return u'{}, {}'.format(self.intervention.number, + self.result) + class AmendmentLog(TimeStampedModel): diff --git a/EquiTrack/partners/tests/test_forms.py b/EquiTrack/partners/tests/test_forms.py new file mode 100644 index 0000000000..ad521db3f7 --- /dev/null +++ b/EquiTrack/partners/tests/test_forms.py @@ -0,0 +1,118 @@ +import datetime +from datetime import timedelta + +from tenant_schemas.test.cases import TenantTestCase + +from django.db.models.fields.related import ManyToManyField + +from EquiTrack.factories import PartnershipFactory, AgreementFactory, ResultStructureFactory +from partners.models import ( + PartnerOrganization, + PCA, + Agreement, + AmendmentLog, + FundingCommitment, + PartnershipBudget, + AgreementAmendmentLog, +) +from partners.forms import AgreementForm + + +def to_dict(instance): + opts = instance._meta + data = {} + for f in opts.concrete_fields + opts.many_to_many: + if isinstance(f, ManyToManyField): + if instance.pk is None: + data[f.name] = [] + else: + data[f.name] = list(f.value_from_object(instance).values_list('pk', flat=True)) + else: + data[f.name] = f.value_from_object(instance) + return data + + +class TestAgreementForm(TenantTestCase): + + def setUp(self): + self.date = datetime.date.today() + self.tenant.country_short_code = 'LEBA' + self.tenant.save() + self.text = 'LEBA/{{}}{}01'.format(self.date.year) + self.agreement = AgreementFactory() + self.result_structure = ResultStructureFactory() + + def create_form(self, data=None, instance=None, user=None): + agr_dict = to_dict(self.agreement) + if data: + for k, v in data.iteritems(): + agr_dict[k] = v + + instance = instance if instance else self.agreement + form = AgreementForm(data=agr_dict, instance=instance) + # form.request.user = user if user else self.agreement.owner + return form + + def test_form_start__date_signed_partner(self): + agr_dict = to_dict(self.agreement) + partner = PartnerOrganization.objects.get(id=self.agreement.partner.id) + partner.partner_type = u'Civil Society Organization' + partner.save() + + agr_dict['agreement_type'] = Agreement.PCA + agr_dict['signed_by_unicef_date'] = self.date - timedelta(days=1) + agr_dict['signed_by_partner_date'] = self.date + # agr_dict['end'] = self.date + timedelta(days=50) + form = self.create_form(data=agr_dict) + self.assertTrue(form.is_valid()) + agr = form.save() + self.assertEqual(agr.start, agr_dict['signed_by_partner_date']) + self.assertIsNotNone(agr.end) + + def test_form_start__date_signed_unicef(self): + agr_dict = to_dict(self.agreement) + partner = PartnerOrganization.objects.get(id=self.agreement.partner.id) + partner.partner_type = u'Civil Society Organization' + partner.save() + + agr_dict['agreement_type'] = Agreement.PCA + agr_dict['signed_by_unicef_date'] = self.date + agr_dict['signed_by_partner_date'] = self.date - timedelta(days=1) + form = self.create_form(data=agr_dict) + self.assertTrue(form.is_valid()) + agr = form.save() + self.assertEqual(agr.start, agr_dict['signed_by_unicef_date']) + self.assertIsNotNone(agr.end) + + def test_start_greater_than_end(self): + agr_dict = to_dict(self.agreement) + partner = PartnerOrganization.objects.get(id=self.agreement.partner.id) + partner.partner_type = u'Civil Society Organization' + partner.save() + + agr_dict['start'] = self.date + timedelta(days=1) + agr_dict['end'] = self.date + form = self.create_form(data=agr_dict) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors['end'][0], + AgreementForm.ERROR_MESSAGES['end_date'] + ) + + def test_start_greater_than_signed_dates(self): + agr_dict = to_dict(self.agreement) + partner = PartnerOrganization.objects.get(id=self.agreement.partner.id) + partner.partner_type = u'Civil Society Organization' + partner.save() + agr_dict['start'] = self.date + agr_dict['end'] = self.date + timedelta(days=10) + agr_dict['signed_by_unicef_date'] = self.date + agr_dict['signed_by_partner_date'] = self.date + timedelta(days=1) + form = self.create_form(data=agr_dict) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors['start'][0], + AgreementForm.ERROR_MESSAGES['start_date_val'] + ) + + diff --git a/EquiTrack/reports/admin.py b/EquiTrack/reports/admin.py index 6d200e0162..7d12b2e7c8 100644 --- a/EquiTrack/reports/admin.py +++ b/EquiTrack/reports/admin.py @@ -157,7 +157,7 @@ def queryset(self, request, queryset): class ResultAdmin(MPTTModelAdmin): form = AutoSizeTextForm - mptt_indent_field = '__unicode__' + mptt_indent_field = 'result_name' search_fields = ( 'wbs', 'name', @@ -169,7 +169,7 @@ class ResultAdmin(MPTTModelAdmin): HiddenResultFilter, ) list_display = ( - '__unicode__', + 'result_name', 'from_date', 'to_date', 'wbs', diff --git a/EquiTrack/reports/models.py b/EquiTrack/reports/models.py index 99890f31a3..266323e919 100644 --- a/EquiTrack/reports/models.py +++ b/EquiTrack/reports/models.py @@ -9,6 +9,7 @@ TimeStampedModel, ) +from django.utils.functional import cached_property # TODO: move to the global schema class ResultStructure(models.Model): @@ -67,6 +68,10 @@ def __unicode__(self): self.name ) +class ResultManager(models.Manager): + def get_queryset(self): + return super(ResultManager, self).get_queryset().select_related('result_structure', 'result_type') + class Result(MPTTModel): @@ -98,9 +103,19 @@ class Result(MPTTModel): hidden = models.BooleanField(default=False) ram = models.BooleanField(default=False) + objects = ResultManager() + class Meta: ordering = ['name'] + @cached_property + def result_name(self): + return u'{} {}: {}'.format( + self.code if self.code else u'', + self.result_type.name, + self.name + ) + def __unicode__(self): return u'{} {}: {}'.format( self.code if self.code else u'', diff --git a/EquiTrack/requirements/base.txt b/EquiTrack/requirements/base.txt index d4bd1b48e8..d3802f8395 100644 --- a/EquiTrack/requirements/base.txt +++ b/EquiTrack/requirements/base.txt @@ -49,7 +49,8 @@ django-analytical==2.0.0 xlrd==0.9.3 dj-static==0.0.6 django-easy-pdf -xhtml2pdf +xhtml2pdf==0.0.6 +html5lib==0.9999999 #must be added after xhtml2pdf reportlab # testing libs diff --git a/EquiTrack/templates/admin/base_site.html b/EquiTrack/templates/admin/base_site.html index 75b5c9bc74..9457f43cc1 100644 --- a/EquiTrack/templates/admin/base_site.html +++ b/EquiTrack/templates/admin/base_site.html @@ -19,12 +19,13 @@ .header #branding { border-right: 0px; - width: 280px; + width: auto; + white-space: nowrap; } .header #branding a { text-decoration: none; - font-weight: 600; + font-weight: 700; } .header #branding h1 { @@ -58,11 +59,18 @@ font-weight: 300; } + #header .logo #test-text { + display: none; + color: #e6e600; + font-weight: 700; + } + .header #user-tools, #country-select { padding: 15px 20px 0 0; float: right; } + /* Footer */ #footer { @@ -84,20 +92,6 @@ } - #testbar { - height:14px; - display:none; - position:relative; - background-color:indianred; - text-align:center; - font-family: 'Open Sans', sans-serif; - font-size:10px; - font-weight: bold; - color: #ffffff; - overflow:hidden; - width:100%; - } - /* ========================================================================== Django Suit Overrides ========================================================================== */ @@ -148,9 +142,15 @@ - {% block extra_head %} {% endblock %} @@ -67,7 +52,6 @@ {% block body %} -
Testing environment
@@ -78,7 +62,7 @@
@@ -323,9 +307,15 @@

You have {{ messages|length }} new messages