Skip to content
This repository has been archived by the owner on Dec 8, 2017. It is now read-only.

Commit

Permalink
Merge 53603c7 into 8e883fc
Browse files Browse the repository at this point in the history
  • Loading branch information
catherinedevlin committed Mar 20, 2015
2 parents 8e883fc + 53603c7 commit 3680f03
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 41 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Expand Up @@ -13,5 +13,5 @@ django-custom-user==0.5
marshmallow==1.2.2
waitress==0.8.9
whitenoise==1.0.6
dateutils==0.6.6
-r requirements/dev.txt
git+https://github.com/niwibe/django-pgjson
2 changes: 1 addition & 1 deletion sbirez/fixtures/alldata.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion sbirez/fixtures/workflowtest.json
@@ -1 +1 @@
[{"pk": 1, "fields": {"name": "dod_proposal_info", "validation": ""}, "model": "sbirez.workflow"}, {"pk": 1, "fields": {"parent": 1, "validation_msg": "Must not exceed 150,000", "ask_if": "", "name": "proposed_cost", "required": false, "human": "", "subworkflow": null, "validation": "int(self) <= 150000", "help": "", "default": "", "data_type": "int", "order": 1}, "model": "sbirez.question"}, {"pk": 2, "fields": {"parent": 1, "validation_msg": "Must not exceed 9 months", "ask_if": "", "name": "duration", "required": false, "human": "", "subworkflow": null, "validation": "", "help": "", "default": "", "data_type": "str", "order": 2}, "model": "sbirez.question"}, {"pk": 3, "fields": {"parent": 1, "validation_msg": "", "ask_if": "", "name": "dta_yn", "required": true, "human": "Does the proposed cost include Discretionary Technical Assistance (DTA?)", "subworkflow": null, "validation": "", "help": "", "default": "False", "data_type": "bool", "order": 3}, "model": "sbirez.question"}, {"pk": 4, "fields": {"parent": 1, "validation_msg": "", "ask_if": "dta_yn", "name": "dta_amount", "required": true, "human": "Proposed DTA amount", "subworkflow": null, "validation": "", "help": "", "default": "0", "data_type": "int", "order": 4}, "model": "sbirez.question"}, {"pk": 5, "fields": {"parent": 1, "validation_msg": "", "ask_if": "dta_yn", "name": "dta_amount", "required": true, "human": "Proposed DTA amount", "subworkflow": null, "validation": "", "help": "", "default": "0", "data_type": "int", "order": 4}, "model": "sbirez.question"}, {"pk": 6, "fields": {"parent": 1, "validation_msg": "", "ask_if": "dta_yn", "name": "proprietary_page_numbers", "required": false, "human": "Proposed DTA amount", "subworkflow": null, "validation": "", "help": "", "default": "", "data_type": "str", "order": 5}, "model": "sbirez.question"}, {"pk": 7, "fields": {"parent": 1, "validation_msg": "", "ask_if": "", "name": "essentially_equivalent_work", "required": false, "human": "Agency name(s) and topic number for essentially equivalent work", "subworkflow": null, "validation": "", "help": "", "default": "", "data_type": "str", "order": 6}, "model": "sbirez.question"}, {"pk": 8, "fields": {"parent": 1, "validation_msg": "", "ask_if": "essentially_equivalent_work", "name": "essentially_equivalent_contract_awarded", "required": false, "human": "Contract number awarded for essentially equivalent work", "subworkflow": null, "validation": "", "help": "", "default": "", "data_type": "str", "order": 7}, "model": "sbirez.question"}]
[{"model": "sbirez.workflow", "fields": {"validation": "", "name": "dod_proposal_info"}, "pk": 1}, {"model": "sbirez.workflow", "fields": {"validation": "", "name": "quest"}, "pk": 2}, {"model": "sbirez.workflow", "fields": {"validation": "", "name": "subquest"}, "pk": 3}, {"model": "sbirez.question", "fields": {"data_type": "int", "validation_msg": "Must not exceed 150,000", "parent": 1, "validation": "lte 150000; gte 0", "name": "proposed_cost", "subworkflow": null, "human": "", "default": "", "required": false, "order": 1, "ask_if": "", "help": ""}, "pk": 1}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "Must not exceed 9 months", "parent": 1, "validation": "lte 9 months; gte 0 months", "name": "duration", "subworkflow": null, "human": "", "default": "", "required": false, "order": 2, "ask_if": "", "help": ""}, "pk": 2}, {"model": "sbirez.question", "fields": {"data_type": "int", "validation_msg": "", "parent": 1, "validation": "gte 0", "name": "dta_amount", "subworkflow": null, "human": "Proposed DTA amount", "default": "0", "required": true, "order": 4, "ask_if": "dta_yn", "help": ""}, "pk": 4}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "", "parent": 1, "validation": "", "name": "proprietary_page_numbers", "subworkflow": null, "human": "Proposed DTA amount", "default": "", "required": false, "order": 5, "ask_if": "dta_yn", "help": ""}, "pk": 6}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "", "parent": 1, "validation": "", "name": "essentially_equivalent_work", "subworkflow": null, "human": "Agency name(s) and topic number for essentially equivalent work", "default": "", "required": false, "order": 6, "ask_if": "", "help": ""}, "pk": 7}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "", "parent": 1, "validation": "", "name": "essentially_equivalent_contract_awarded", "subworkflow": null, "human": "Contract number awarded for essentially equivalent work", "default": "", "required": false, "order": 7, "ask_if": "essentially_equivalent_work", "help": ""}, "pk": 8}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "Prithee speak thy name", "parent": 2, "validation": "", "name": "quest_thy_name", "subworkflow": null, "human": "What is your name?", "default": "", "required": true, "order": 1, "ask_if": "", "help": "'Thy moniker'"}, "pk": 10}, {"model": "sbirez.question", "fields": {"data_type": "workflow", "validation_msg": null, "parent": 2, "validation": null, "name": "quest_subflow", "subworkflow": 3, "human": null, "default": null, "required": true, "order": 2, "ask_if": null, "help": null}, "pk": 11}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": null, "parent": 3, "validation": null, "name": "quest_thy_quest", "subworkflow": null, "human": "What is thy quest?", "default": null, "required": false, "order": 1, "ask_if": null, "help": "'That which thou seekest'"}, "pk": 12}, {"model": "sbirez.question", "fields": {"data_type": "str", "validation_msg": "Lancelot already said blue", "parent": 3, "validation": "does_not_equal blue", "name": "quest_thy_favorite_color", "subworkflow": null, "human": "What is thy favority color?", "default": null, "required": true, "order": 2, "ask_if": null, "help": "preferred band in the electromagnetic spectrum"}, "pk": 13}]
3 changes: 1 addition & 2 deletions sbirez/migrations/0013_auto_20150317_1907.py
Expand Up @@ -2,7 +2,6 @@
from __future__ import unicode_literals

from django.db import models, migrations
from django_pgjson.fields import JsonField
import djorm_pgfulltext.fields
from django.conf import settings

Expand All @@ -19,7 +18,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('submitted_at', models.DateTimeField(auto_now=True)),
('data', JsonField()),
('data', models.TextField(null=True)),
('firm', models.ForeignKey(to='sbirez.Firm', related_name='proposals')),
('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='proposals')),
('topic', models.ForeignKey(to='sbirez.Topic', related_name='proposals')),
Expand Down
69 changes: 69 additions & 0 deletions sbirez/migrations/0014_auto_20150319_1730.py
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import djorm_pgfulltext.fields


class Migration(migrations.Migration):

dependencies = [
('sbirez', '0013_auto_20150317_1907'),
]

operations = [
migrations.AlterField(
model_name='proposal',
name='data',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='ask_if',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='data_type',
field=models.TextField(null=True, default='str'),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='default',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='help',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='human',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='required',
field=models.NullBooleanField(default=False),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='validation',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='question',
name='validation_msg',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
]
20 changes: 10 additions & 10 deletions sbirez/models.py
Expand Up @@ -4,7 +4,7 @@
from djorm_pgfulltext.models import SearchManager
from django.conf import settings
from custom_user.models import AbstractEmailUser
from django_pgjson.fields import JsonField
from rest_framework import serializers

class Address(models.Model):
street = models.TextField()
Expand Down Expand Up @@ -113,14 +113,14 @@ class Question(models.Model):
parent = models.ForeignKey(Workflow, related_name='questions')

# Each question should be EITHER an actual question...
data_type = models.TextField(default='str')
required = models.BooleanField(default=False)
default = models.TextField(blank=True)
human = models.TextField(blank=True)
help = models.TextField(blank=True)
validation = models.TextField(blank=True)
validation_msg = models.TextField(blank=True)
ask_if = models.TextField(blank=True)
data_type = models.TextField(null=True, default='str')
required = models.NullBooleanField(default=False)
default = models.TextField(null=True, blank=True)
human = models.TextField(null=True, blank=True)
help = models.TextField(null=True, blank=True)
validation = models.TextField(null=True, blank=True)
validation_msg = models.TextField(null=True, blank=True)
ask_if = models.TextField(null=True, blank=True)

# ... OR a sub-workflow
subworkflow = models.ForeignKey(Workflow, related_name='subworkflow_of', null=True, blank=True)
Expand All @@ -135,4 +135,4 @@ class Proposal(models.Model):
workflow = models.ForeignKey(Workflow, related_name='proposals')
topic = models.ForeignKey(Topic, related_name='proposals')
submitted_at = models.DateTimeField(auto_now=True)
data = JsonField()
data = models.TextField(null=True, blank=True)
46 changes: 40 additions & 6 deletions sbirez/serializers.py
@@ -1,3 +1,6 @@
import json
import shlex
from sbirez import validation_helpers
from django.contrib.auth.models import User, Group
from sbirez.models import Topic, Reference, Phase, Keyword, Area, Firm, Person
from sbirez.models import Address, Workflow, Question, Proposal, Address
Expand Down Expand Up @@ -203,16 +206,47 @@ class Meta:
fields = ('name', 'validation', 'questions', )


def _validate_question(data, question):

if question.required and not question.subworkflow:
if (question.name not in data):
raise serializers.ValidationError(
'Required field %s absent' % question.name)
if (hasattr(data[question.name], 'strip') and
not data[question.name].strip()):
raise serializers.ValidationError(
'Required field %s absent' % question.name)

if question.validation:
for validation in question.validation.split(';'):
args = shlex.split(validation)
function_name = args.pop(0)
func = getattr(validation_helpers, function_name)
if question.name in data:
if not func(data, data[question.name], *args):
raise serializers.ValidationError(
'%s: %s' % (question.name, question.validation_msg))

if question.subworkflow:
for subquestion in question.subworkflow.questions.all():
_validate_question(data, subquestion)

def genericValidator(proposal):
'''
Inspect the workflow's validators and apply them to
the proposal's data
'''
data = json.loads(proposal['data'])

for question in proposal['workflow'].questions.all():
_validate_question(data, question)


class ProposalSerializer(serializers.ModelSerializer):
data = serializers.SerializerMethodField('clean_data')

class Meta:
model = Proposal
fields = ('owner', 'firm', 'workflow', 'topic',
'submitted_at', 'data')

def clean_data(self, obj):
return obj.data
validators = [genericValidator]


class AddressSerializer(serializers.ModelSerializer):
Expand Down
85 changes: 65 additions & 20 deletions sbirez/test_api.py
Expand Up @@ -741,30 +741,75 @@ class ProposalTests(APITestCase):

fixtures = ['alldata.json']

def _deserialize_data(self, response):
"""An ugly hack for the fact that the 'data' field comes back
serialized."""
response.data['data'] = json.loads(response.data['data'])

# Check that the proposal index loads
def test_proposal_view_set(self):
response = self.client.get('/api/v1/proposals/')
self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(response.data["count"], 1)
self.assertEqual(response.data["count"], 2)

def test_get_one_proposal(self):
response = self.client.get('/api/v1/proposals/1/')
response = self.client.get('/api/v1/proposals/2/')
self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(response.data["data"]["essentially_equivalent_work"],
'USAF ABCDEF')

def test_proposal_data_deserialized(self):
response = self.client.get('/api/v1/proposals/1/')
data = response.data['data']
self.assertEqual(type(data), dict)

def test_update_proposal(self):
response = self.client.get('/api/v1/proposals/1/')
data = response.data['data']
self.assertEqual(data['duration'], '3 yr')
data['duration'] = '1 yr'
response = self.client.put('/api/v1/proposals/1/', {})
response = self.client.get('/api/v1/proposals/1/')
self.assertEqual(data['duration'], '1 yr')


self._deserialize_data(response)
self.assertEqual(response.data["data"]["quest_thy_name"],
'Galahad')

def test_good_update_proposal(self):
response = self.client.get('/api/v1/proposals/2/')
self._deserialize_data(response)
self.assertEqual(response.data['data']['quest_thy_favorite_color'],
'yellow')

response.data['data']['quest_thy_favorite_color'] = 'green'
response.data['data'] = json.dumps(response.data['data'])
response = self.client.put('/api/v1/proposals/2/', response.data)
self.assertEqual(status.HTTP_200_OK, response.status_code)

response = self.client.get('/api/v1/proposals/2/')
self._deserialize_data(response)
self.assertEqual(response.data['data']['quest_thy_favorite_color'],
'green')

def test_bad_update_proposal(self):
response = self.client.get('/api/v1/proposals/2/')
self._deserialize_data(response)
response.data['data']['quest_thy_favorite_color'] = 'blue'
response.data['data'] = json.dumps(response.data['data'])
response = self.client.put('/api/v1/proposals/2/', response.data)
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
self.assertIn('quest_thy_favorite_color: Lancelot already said blue',
response.data['non_field_errors'])

# omit a required field
def test_incomplete_post_raises_error(self):
response = self.client.post('/api/v1/proposals/',
{'owner': 2, 'firm': 1, 'workflow': 2,
'topic': 1, 'data': json.dumps(
{
"quest_thy_quest": "To seek the Grail",
"quest_thy_favorite_color": "#0000FF"})
})
self.assertIn('Required field quest_thy_name absent',
response.data['non_field_errors'])

def test_post_full_proposal(self):
response = self.client.post('/api/v1/proposals/',
{'owner': 2, 'firm': 1, 'workflow': 2,
'topic': 1, 'data': json.dumps(
{"quest_thy_name": "Galahad",
"quest_thy_quest": "To seek the Grail",
"quest_thy_favorite_color": "#0000FF"})
})
self.assertEqual(status.HTTP_201_CREATED, response.status_code)

response = self.client.get('/api/v1/proposals/%s/' % response.data['id'])
self.assertEqual(status.HTTP_200_OK, response.status_code)
self._deserialize_data(response)
self.assertEqual(response.data['data']['quest_thy_favorite_color'],
"#0000FF")

15 changes: 15 additions & 0 deletions sbirez/validation_helpers.py
@@ -0,0 +1,15 @@
from dateutil.parser import parse

def no_more_than(data, field, limit, units=None):
"""`field` Less Than or Equal to `limit`"""
return float(field) <= limit

def no_less_than(data, field, limit, units=None):
"""`field` Less Than or Equal to `limit`"""
return float(field) <= limit

def does_not_equal(data, field, target):
return field.strip().lower() != target.strip().lower()

lte = no_more_than
gte = no_less_than

0 comments on commit 3680f03

Please sign in to comment.