From f6866c8318bbd63a148b6354e76bd18ddbb125b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mu=C3=B1oz=20C=C3=A1rdenas?= Date: Thu, 22 Mar 2018 14:34:27 +0100 Subject: [PATCH 1/3] Reorder WorkflowLevel2 fields Reorder alphabetically separating Foreign/ManyToMany fields. --- workflow/models.py | 56 ++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/workflow/models.py b/workflow/models.py index f1338287e..0454376be 100755 --- a/workflow/models.py +++ b/workflow/models.py @@ -1108,44 +1108,46 @@ def get_queryset(self): class WorkflowLevel2(models.Model): - level2_uuid = models.CharField(max_length=255, verbose_name='WorkflowLevel2 UUID', default=uuid.uuid4, unique=True, blank=True, help_text="Unique ID") - workflowlevel1 = models.ForeignKey(WorkflowLevel1, verbose_name="Program", related_name="workflowlevel2", help_text="Primary Workflow") - parent_workflowlevel2 = models.IntegerField("Parent", default=0, blank=True) - milestone = models.ForeignKey("Milestone", null=True, blank=True, on_delete=models.SET_NULL, help_text="Association with a Workflow Level 1 Milestone") - name = models.CharField("Name",max_length=255) - sector = models.ForeignKey("Sector", verbose_name="Sector", blank=True, null=True, related_name="workflow2_sector", on_delete=models.SET_NULL, help_text="Primary Sector or type of work") - sub_sector = models.ManyToManyField("Sector", verbose_name="Sub-Sector", blank=True, related_name="workflowlevel2_sub_sector", help_text="Secondary sector or type of work") - description = models.TextField("Description", blank=True, null=True, help_text="Description of the overall effort") - site = models.ManyToManyField(SiteProfile, blank=True, help_text="Geographic sites or locations") - short_name = models.CharField("Code", max_length=20, blank=True, null=True , help_text="Shortened name autogenerated") - office = models.ForeignKey(Office, verbose_name="Office", null=True, blank=True, on_delete=models.SET_NULL, help_text="Primary office for effort") - staff_responsible = models.ForeignKey(TolaUser, on_delete=models.SET_NULL, blank=True, null=True, help_text="Responsible party") - stakeholder = models.ManyToManyField(Stakeholder, verbose_name="Stakeholders", blank=True, help_text="Other parties involved in effort") - effect_or_impact = models.TextField("What is the anticipated Outcome or Goal?", blank=True, null=True, help_text="Descriptive, what is the anticipated outcome of the effort") - expected_start_date = models.DateTimeField("Expected starting date", blank=True, null=True) - expected_end_date = models.DateTimeField("Expected ending date", blank=True, null=True) - total_estimated_budget = models.DecimalField("Total Project Budget", decimal_places=2, max_digits=12, help_text="Total budget to date calculated from Budget Module", default=Decimal("0.00"), blank=True) - local_currency = models.ForeignKey(Currency, null=True, blank=True, related_name="local_project", on_delete=models.SET_NULL, help_text="Primary Currency") - donor_currency = models.ForeignKey(Currency, null=True, blank=True, related_name="donor_project", on_delete=models.SET_NULL, help_text="Secondary Currency") - approval = models.ManyToManyField(ApprovalWorkflow, blank=True, help_text="Multiple approval level and users") - justification_background = models.TextField("General Background and Problem Statement", blank=True, null=True, help_text="Descriptive, why are we starting this effort") - risks_assumptions = models.TextField("Risks and Assumptions", blank=True, null=True, help_text="Descriptive, what are the risks associated") - description_of_government_involvement = models.TextField(blank=True, null=True, help_text="Descriptive, what government entities might be involved") - description_of_community_involvement = models.TextField(blank=True, null=True, help_text="Descriptive, what community orgs are groups are involved") actual_start_date = models.DateTimeField(blank=True, null=True) actual_end_date = models.DateTimeField(blank=True, null=True) actual_duration = models.CharField(max_length=255, blank=True, null=True) actual_cost = models.DecimalField("Actual Cost", decimal_places=2, max_digits=20, default=Decimal("0.00"), blank=True, help_text="Cost to date calculated from Budget Module") - total_cost = models.DecimalField("Estimated Budget for Organization", decimal_places=2, max_digits=12, help_text="In USD", default=Decimal("0.00"), blank=True) capacity_built = models.TextField("Describe how sustainability was ensured for this project?", max_length=755, blank=True, null=True, help_text="Descriptive, did this help increases internal or external capacity") - quality_assured = models.TextField("How was quality assured for this project", max_length=755, blank=True, null=True, help_text="Descriptive, how was the overall quality assured for this effort") + description = models.TextField("Description", blank=True, null=True, help_text="Description of the overall effort") + description_of_community_involvement = models.TextField(blank=True, null=True, help_text="Descriptive, what community orgs are groups are involved") + description_of_government_involvement = models.TextField(blank=True, null=True, help_text="Descriptive, what government entities might be involved") + expected_end_date = models.DateTimeField("Expected ending date", blank=True, null=True) + expected_start_date = models.DateTimeField("Expected starting date", blank=True, null=True) issues_and_challenges = models.TextField("List any issues or challenges faced (include reasons for delays)", blank=True, null=True, help_text="Descriptive, what are some of the issues and challenges") + justification_background = models.TextField("General Background and Problem Statement", blank=True, null=True, help_text="Descriptive, why are we starting this effort") lessons_learned = models.TextField("Lessons learned", blank=True, null=True, help_text="Descriptive, when completed what lessons were learned") + level2_uuid = models.CharField(max_length=255, verbose_name='WorkflowLevel2 UUID', default=uuid.uuid4, unique=True, blank=True, help_text="Unique ID") + name = models.CharField("Name",max_length=255) + parent_workflowlevel2 = models.IntegerField("Parent", default=0, blank=True) + quality_assured = models.TextField("How was quality assured for this project", max_length=755, blank=True, null=True, help_text="Descriptive, how was the overall quality assured for this effort") + risks_assumptions = models.TextField("Risks and Assumptions", blank=True, null=True, help_text="Descriptive, what are the risks associated") + short_name = models.CharField("Code", max_length=20, blank=True, null=True , help_text="Shortened name autogenerated") + total_cost = models.DecimalField("Estimated Budget for Organization", decimal_places=2, max_digits=12, help_text="In USD", default=Decimal("0.00"), blank=True) + total_estimated_budget = models.DecimalField("Total Project Budget", decimal_places=2, max_digits=12, help_text="Total budget to date calculated from Budget Module", default=Decimal("0.00"), blank=True) + + approval = models.ManyToManyField(ApprovalWorkflow, blank=True, help_text="Multiple approval level and users") + donor_currency = models.ForeignKey(Currency, null=True, blank=True, related_name="donor_project", on_delete=models.SET_NULL, help_text="Secondary Currency") + effect_or_impact = models.TextField("What is the anticipated Outcome or Goal?", blank=True, null=True, help_text="Descriptive, what is the anticipated outcome of the effort") indicators = models.ManyToManyField("indicators.Indicator", blank=True) + local_currency = models.ForeignKey(Currency, null=True, blank=True, related_name="local_project", on_delete=models.SET_NULL, help_text="Primary Currency") + milestone = models.ForeignKey("Milestone", null=True, blank=True, on_delete=models.SET_NULL, help_text="Association with a Workflow Level 1 Milestone") + office = models.ForeignKey(Office, verbose_name="Office", null=True, blank=True, on_delete=models.SET_NULL, help_text="Primary office for effort") # products = OneToMany(Product) # reverse related relationship + sector = models.ForeignKey("Sector", verbose_name="Sector", blank=True, null=True, related_name="workflow2_sector", on_delete=models.SET_NULL, help_text="Primary Sector or type of work") + site = models.ManyToManyField(SiteProfile, blank=True, help_text="Geographic sites or locations") + staff_responsible = models.ForeignKey(TolaUser, on_delete=models.SET_NULL, blank=True, null=True, help_text="Responsible party") + stakeholder = models.ManyToManyField(Stakeholder, verbose_name="Stakeholders", blank=True, help_text="Other parties involved in effort") + sub_sector = models.ManyToManyField("Sector", verbose_name="Sub-Sector", blank=True, related_name="workflowlevel2_sub_sector", help_text="Secondary sector or type of work") + workflowlevel1 = models.ForeignKey(WorkflowLevel1, verbose_name="Program", related_name="workflowlevel2", help_text="Primary Workflow") + create_date = models.DateTimeField("Date Created", null=True, blank=True) - edit_date = models.DateTimeField("Last Edit Date", null=True, blank=True) created_by = models.ForeignKey('auth.User', related_name='workflowlevel2', null=True, blank=True, on_delete=models.SET_NULL) + edit_date = models.DateTimeField("Last Edit Date", null=True, blank=True) history = HistoricalRecords() objects = WorkflowLevel2Manager() From 6e9718b447cfea1c0bfa3db477c0f6e7bfb72c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mu=C3=B1oz=20C=C3=A1rdenas?= Date: Thu, 22 Mar 2018 15:49:08 +0100 Subject: [PATCH 2/3] Add new fields to WorkflowLevel2 --- requirements/pkg.txt | 1 + tola/management/commands/loadinitialdata.py | 4 +- .../commands/tests/test_loadinitialdata.py | 4 +- .../migrations/0021_auto_20180323_0215.py | 58 +++++++++++++++++++ workflow/models.py | 29 +++++++++- workflow/signals.py | 11 +++- workflow/tests/test_models.py | 22 +++++++ 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 workflow/migrations/0021_auto_20180323_0215.py diff --git a/requirements/pkg.txt b/requirements/pkg.txt index f028f9fb3..1a4b491e3 100644 --- a/requirements/pkg.txt +++ b/requirements/pkg.txt @@ -4,3 +4,4 @@ django-oauth-toolkit==1.0.0 django-simple-history==1.9.0 elasticsearch==5.4.0 factory_boy==2.9.2 +voluptuous==0.11.1 diff --git a/tola/management/commands/loadinitialdata.py b/tola/management/commands/loadinitialdata.py index 83231c369..cd0f3565a 100644 --- a/tola/management/commands/loadinitialdata.py +++ b/tola/management/commands/loadinitialdata.py @@ -6,7 +6,7 @@ from django.conf import settings from django.contrib.sites.models import Site from django.contrib.sites.shortcuts import get_current_site -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.management import call_command from django.core.management.base import BaseCommand from django.db import transaction, IntegrityError, connection @@ -2961,7 +2961,7 @@ def handle(self, *args, **options): self._create_collected_data() self._create_workflowlevel1_sectors() self._create_workflowteams() - except IntegrityError: + except (IntegrityError, ValidationError): msg = ("Error: the data could not be populated in the " "database. Check that the affected database tables are " "empty.") diff --git a/tola/management/commands/tests/test_loadinitialdata.py b/tola/management/commands/tests/test_loadinitialdata.py index 54f2dc667..7caf36a8e 100644 --- a/tola/management/commands/tests/test_loadinitialdata.py +++ b/tola/management/commands/tests/test_loadinitialdata.py @@ -2,7 +2,7 @@ import sys from django.contrib.auth.models import Group, User -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.management import call_command from django.db import IntegrityError, connection from django.test import TestCase, override_settings, tag @@ -109,7 +109,7 @@ def test_load_demo_data_two_times_crashes_but_db_keeps_consistent(self): User.objects.all().delete() - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): call_command('loadinitialdata', *args, **opts) self.assertRaises(User.DoesNotExist, User.objects.get, diff --git a/workflow/migrations/0021_auto_20180323_0215.py b/workflow/migrations/0021_auto_20180323_0215.py new file mode 100644 index 000000000..2ec5d265f --- /dev/null +++ b/workflow/migrations/0021_auto_20180323_0215.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-03-23 09:15 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.hstore +from django.contrib.postgres.operations import HStoreExtension +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflow', '0020_product'), + ] + + operations = [ + HStoreExtension(), # https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/fields/#hstorefield + migrations.AddField( + model_name='historicalworkflowlevel2', + name='address', + field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, help_text='Address object with the structure: street (string), house_number (string), postal_code: (string), city (string), country (string)', null=True), + ), + migrations.AddField( + model_name='historicalworkflowlevel2', + name='notes', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='historicalworkflowlevel2', + name='site_instructions', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='historicalworkflowlevel2', + name='type', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AddField( + model_name='workflowlevel2', + name='address', + field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, help_text='Address object with the structure: street (string), house_number (string), postal_code: (string), city (string), country (string)', null=True), + ), + migrations.AddField( + model_name='workflowlevel2', + name='notes', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='workflowlevel2', + name='site_instructions', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='workflowlevel2', + name='type', + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/workflow/models.py b/workflow/models.py index 0454376be..4edfbe4ef 100755 --- a/workflow/models.py +++ b/workflow/models.py @@ -4,12 +4,15 @@ from django.contrib.postgres import fields from django.contrib.auth.models import User, Group from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError from decimal import Decimal import uuid from django.conf import settings +from django.contrib.postgres.fields import HStoreField from simple_history.models import HistoricalRecords from django.contrib.postgres.fields import JSONField +from voluptuous import Schema, All, Any, Length from search.utils import ElasticsearchIndexer @@ -1112,6 +1115,7 @@ class WorkflowLevel2(models.Model): actual_end_date = models.DateTimeField(blank=True, null=True) actual_duration = models.CharField(max_length=255, blank=True, null=True) actual_cost = models.DecimalField("Actual Cost", decimal_places=2, max_digits=20, default=Decimal("0.00"), blank=True, help_text="Cost to date calculated from Budget Module") + address = HStoreField(blank=True, null=True, help_text="Address object with the structure: street (string), house_number (string), postal_code: (string), city (string), country (string)") capacity_built = models.TextField("Describe how sustainability was ensured for this project?", max_length=755, blank=True, null=True, help_text="Descriptive, did this help increases internal or external capacity") description = models.TextField("Description", blank=True, null=True, help_text="Description of the overall effort") description_of_community_involvement = models.TextField(blank=True, null=True, help_text="Descriptive, what community orgs are groups are involved") @@ -1122,13 +1126,16 @@ class WorkflowLevel2(models.Model): justification_background = models.TextField("General Background and Problem Statement", blank=True, null=True, help_text="Descriptive, why are we starting this effort") lessons_learned = models.TextField("Lessons learned", blank=True, null=True, help_text="Descriptive, when completed what lessons were learned") level2_uuid = models.CharField(max_length=255, verbose_name='WorkflowLevel2 UUID', default=uuid.uuid4, unique=True, blank=True, help_text="Unique ID") - name = models.CharField("Name",max_length=255) + name = models.CharField("Name", max_length=255) + notes = models.TextField(blank=True, null=True) parent_workflowlevel2 = models.IntegerField("Parent", default=0, blank=True) quality_assured = models.TextField("How was quality assured for this project", max_length=755, blank=True, null=True, help_text="Descriptive, how was the overall quality assured for this effort") risks_assumptions = models.TextField("Risks and Assumptions", blank=True, null=True, help_text="Descriptive, what are the risks associated") - short_name = models.CharField("Code", max_length=20, blank=True, null=True , help_text="Shortened name autogenerated") + short_name = models.CharField("Code", max_length=20, blank=True, null=True, help_text="Shortened name autogenerated") + site_instructions = models.TextField(blank=True, null=True) total_cost = models.DecimalField("Estimated Budget for Organization", decimal_places=2, max_digits=12, help_text="In USD", default=Decimal("0.00"), blank=True) total_estimated_budget = models.DecimalField("Total Project Budget", decimal_places=2, max_digits=12, help_text="Total budget to date calculated from Budget Module", default=Decimal("0.00"), blank=True) + type = models.CharField(max_length=50, blank=True, null=True) approval = models.ManyToManyField(ApprovalWorkflow, blank=True, help_text="Multiple approval level and users") donor_currency = models.ForeignKey(Currency, null=True, blank=True, related_name="donor_project", on_delete=models.SET_NULL, help_text="Secondary Currency") @@ -1189,6 +1196,24 @@ class Meta: ("can_approve", "Can approve initiation"), ) + def _validate_address(self, address): + schema = Schema({ + 'street': All(Any(str, unicode), Length(max=100)), + 'house_number': All(Any(str, unicode), Length(max=20)), + 'postal_code': All(Any(str, unicode), Length(max=20)), + 'city': All(Any(str, unicode), Length(max=85)), + 'country': All(Any(str, unicode), Length(max=50)), + }) + schema(address) + + def clean_fields(self, exclude=None): + super(WorkflowLevel2, self).clean_fields(exclude=exclude) + if self.address: + try: + self._validate_address(self.address) + except Exception as error: + raise ValidationError(error) + def save(self, *args, **kwargs): if self.create_date is None: self.create_date = timezone.now() diff --git a/workflow/signals.py b/workflow/signals.py index 1046e2154..71fba131f 100644 --- a/workflow/signals.py +++ b/workflow/signals.py @@ -14,13 +14,18 @@ from tola import DEMO_BRANCH, track_sync as tsync from tola.management.commands.loadinitialdata import DEFAULT_WORKFLOW_LEVEL_1S from workflow.models import (Organization, TolaUser, WorkflowLevel1, - WorkflowTeam, ROLE_ORGANIZATION_ADMIN, - ROLE_PROGRAM_ADMIN, ROLE_PROGRAM_TEAM, - ROLE_VIEW_ONLY) + WorkflowTeam, WorkflowLevel2, + ROLE_ORGANIZATION_ADMIN, ROLE_PROGRAM_ADMIN, + ROLE_PROGRAM_TEAM, ROLE_VIEW_ONLY) logger = logging.getLogger(__name__) +@receiver(signals.pre_save, sender=WorkflowLevel2) +def pre_save_handler(sender, instance, *args, **kwargs): + instance.full_clean() + + def get_addon_by_id(addon_id, addons): for addon in addons: if addon.id == addon_id: diff --git a/workflow/tests/test_models.py b/workflow/tests/test_models.py index 6cf065443..93505f6d2 100644 --- a/workflow/tests/test_models.py +++ b/workflow/tests/test_models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from django.core.exceptions import ValidationError from django.test import TestCase, override_settings, tag import factories @@ -89,3 +90,24 @@ class WorkflowLevel2Test(TestCase): def test_print_instance(self): wflvl2 = factories.WorkflowLevel2.build() self.assertEqual(unicode(wflvl2), u'Help Syrians') + + def test_save_address_fail(self): + wflvl2 = factories.WorkflowLevel2() + wflvl2.address = { + 'street': None, + } + self.assertRaises(ValidationError, wflvl2.save) + + wflvl2.address = { + 'house_number': 'a'*21, + } + self.assertRaises(ValidationError, wflvl2.save) + + def test_save_address(self): + factories.WorkflowLevel2(address={ + 'street': 'Oderberger Straße', + 'house_number': '16A', + 'postal_code': '10435', + 'city': 'Berlin', + 'country': 'Germany', + }) From 39223bdd443119a525dd048b4cdcfd4c36e39f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mu=C3=B1oz=20C=C3=A1rdenas?= Date: Fri, 23 Mar 2018 11:47:58 +0100 Subject: [PATCH 3/3] Reorder imports --- workflow/models.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/workflow/models.py b/workflow/models.py index 4edfbe4ef..d4b309c59 100755 --- a/workflow/models.py +++ b/workflow/models.py @@ -1,26 +1,23 @@ from __future__ import unicode_literals +from decimal import Decimal +import uuid from django.db import models +from django.conf import settings from django.contrib.postgres import fields from django.contrib.auth.models import User, Group +from django.contrib.postgres.fields import HStoreField, JSONField from django.contrib.sites.models import Site from django.core.exceptions import ValidationError -from decimal import Decimal -import uuid - -from django.conf import settings -from django.contrib.postgres.fields import HStoreField -from simple_history.models import HistoricalRecords -from django.contrib.postgres.fields import JSONField -from voluptuous import Schema, All, Any, Length - -from search.utils import ElasticsearchIndexer - +from django.db.models import Q try: from django.utils import timezone except ImportError: from datetime import datetime as timezone -from django.db.models import Q +from simple_history.models import HistoricalRecords +from voluptuous import Schema, All, Any, Length + +from search.utils import ElasticsearchIndexer ROLE_ORGANIZATION_ADMIN = 'OrgAdmin' ROLE_PROGRAM_ADMIN = 'ProgramAdmin'