Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
andymckay committed May 16, 2009
0 parents commit afd7fce
Show file tree
Hide file tree
Showing 47 changed files with 1,334 additions and 0 deletions.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include README.rst
recursive-include malnutrition/ui/static *
recursive-include malnutrition/ui/templates *
73 changes: 73 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
This is a series of re-usable modules for the Kenyan and Malawi Malnutrition projects.

forms
---------------------

Contains a simple SMS form handler. Based on Django's forms, but doesn't have the same API, it specifies how the form will
look, then takes the text and validates the input. The format is similar to Django forms::

class ReportForm(Form):
sick = BooleanField()
id = StringField(required=True, valid="(\d+)")
This means we expect a y or n followed by a number.

To process it pass in data eg::

rf = ReportForm("y 19122008")

Was it valid?::

rf.is_valid()
If not, there's a list of errors in the .errors attribute::

rf.errors

You can also find the error on the field::

rf.sick.error
And finally the parsed (eg: dates into datetime fields) value is there in data::

rf.sick.data
You can make fields required, by passing through required=True in the Field construction.

models
---------------------

Models are some standard models, we've been reusing. These aren't included in your project, you have
to include them, however that is pretty easy since they are all abstract. For example::

from malnutrition.models import case
class Case(case.Case):
pass
A few notes on this:

- Since the models are related to each other, you will likely get errors unless you pull all of them in and name them: Case, Provider, Facility, Zone

- Report requires patient and provider

- Log is standalone and requires nothing

ui
------------------------

A base reusable user interface allows you to start with a base login, logout and search etc without having to specify this in the project.

For example::

# add in rapidsms_baseui
INSTALLED_APPS = list(INSTALLED_APPS)
INSTALLED_APPS.append('malnutrition.ui')

Add in the URLs to your urls.py::

urlpatterns = patterns('',
(r'^', include('malnutrition.ui.urls')),
)

And that should be it.
2 changes: 2 additions & 0 deletions malnutrition/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VERSION = (0, 1, 0)
__version__ = '.'.join(map(str, VERSION))
6 changes: 6 additions & 0 deletions malnutrition/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# I found my self writing lots of forms for SMS and handling the validation
# which was a bit annoying
# # sort of based on django forms, this is a form library that takes the incoming text
# in an sms, assumes that its a series of fields, then parses the text into those fields
from forms import Form
from fields import StringField, GenderField, BooleanField, DateField
54 changes: 54 additions & 0 deletions malnutrition/forms/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from forms import Field, FieldError
import re

class StringField(Field):
def __init__(self, valid=None, required=False):
self.rx = None
if valid:
self.rx = re.compile("^%s$" % valid)
Field.__init__(self, required=required)

def validate(self, text):
if not self.rx:
self.valid = True
else:
if self.rx.match(text):
self.valid = True
self.data = text
else:
raise FieldError, "The field %s did not match the required format." % (self.name)

class GenderField(Field):
def __init__(self, required=False):
Field.__init__(self, required=required)

def validate(self, text):
if text.lower() not in ["m", "f"]:
raise FieldError, "The field %s was not formatted correctly, got %s" % (self.name, text)
self.valid = True
self.data = text.lower()

class BooleanField(Field):
def __init__(self, required=False):
Field.__init__(self, required=required)

def validate(self, text):
if text.lower() not in ["y", "n"]:
raise FieldError, "The field %s was not formatted correctly, got %s" % (self.name, text)
self.valid = True
self.data = text.lower() == "y"

class DateField(Field):
def __init__(self, format="%d/%m/%Y", required=False):
self.format = format
Field.__init__(self, required=required)

def validate(self, text):
try:
if isinstance(text, str):
self.data = datetime.strptime(text, self.format).date()
else:
self.data = text
except ValueError, e:
raise FieldError, "The field %s was not formatted correctly, got %s, expecting in format %s" % (self.name, text, self.format)
self.valid = True
66 changes: 66 additions & 0 deletions malnutrition/forms/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from datetime import datetime, date
import re

class FieldError(Exception):
pass

class Field:
creation_counter = 0

def __init__(self, required=None):
assert required is not None
self.name = None
self.required = required
self.data = None
self.valid = False
self.error = None

self.creation_counter = Field.creation_counter
Field.creation_counter += 1

class Form(object):
def __init__(self, text):
self.fields = []
self.errors = []
self.sep = " "

for item in dir(self):
obj = getattr(self, item)
if issubclass(obj.__class__, Field):
obj.name = item
self.fields.append([obj.creation_counter, obj])

self.fields.sort()
self.fields = [ f[1] for f in self.fields ]

self.__call__(text)

def __call__(self, text):
text = text.strip().split(self.sep)

for bit, field in map(None, text, self.fields):
if field is None:
self.errors.append("The text was longer than the form.")
continue
if not bit:
if field.required:
msg = "The field %s is required" % field.name
field.error = msg
self.errors.append(msg)
continue

try:
if hasattr(field, "parser"):
bit = field.parser(bit)
field.validate(bit)
except FieldError, e:
field.error = str(e)
self.errors.append(str(e))

if not self.errors:
self.valid = True

def is_valid(self):
return not bool(self.errors)


29 changes: 29 additions & 0 deletions malnutrition/forms/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest

from datetime import datetime, date
import re

from forms import Form
from fields import BooleanField, StringField, DateField

def parser(text):
return date(year=int(text[4:8]),day=int(text[0:2]),month=int(text[2:4]))

class test(unittest.TestCase):
# These need to be expanded

def testSimple(self):
class ReportForm(Form):
male = BooleanField()
date = DateField(format="%m%d%Y")
date.parser = parser
id = StringField(required=True, valid="(\d+)")

rf = ReportForm("y 19122008 123")
assert rf.is_valid()
assert rf.male.data
assert rf.date.data.month == 12
assert not rf.errors

if __name__=="__main__":
unittest.main()
Empty file added malnutrition/models/__init__.py
Empty file.
77 changes: 77 additions & 0 deletions malnutrition/models/case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

from datetime import datetime

class Case(models.Model):
GENDER_CHOICES = (
('M', _('Male')),
('F', _('Female')),
)

ref_id = models.IntegerField(_('Case ID #'), null=True, db_index=True)
first_name = models.CharField(max_length=255, db_index=True)
last_name = models.CharField(max_length=255, db_index=True)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
dob = models.DateField(_('Date of Birth'))
guardian = models.CharField(max_length=255, null=True, blank=True)
mobile = models.CharField(max_length=16, null=True, blank=True)
provider = models.ForeignKey("Provider", db_index=True)
zone = models.ForeignKey("Zone", null=True, db_index=True)
village = models.CharField(max_length=255, null=True, blank=True)
district = models.CharField(max_length=255, null=True, blank=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()

def __unicode__ (self):
return "#%d" % self.ref_id

def _luhn(self, x):
parity = True
sum = 0
for c in reversed(str(x)):
n = int(c)
if parity:
n *= 2
if n > 9: n -= 9
sum += n
return x * 10 + 10 - sum % 10

def save(self, *args):
if not self.id:
self.created_at = self.updated_at = datetime.now()
else:
self.updated_at = datetime.now()
super(Case, self).save(*args)
if not self.ref_id:
self.ref_id = self._luhn(self.id)
super(Case, self).save(*args)

def get_dictionary(self):
""" Return the data as a generic dictionary with some useful convenience methods done """
return {
'ref_id': self.ref_id,
'last_name': self.last_name.upper(),
'first_name': self.first_name,
'first_name_short': self.first_name.upper()[0],
'gender': self.gender.upper()[0],
'months': self.age(),
'guardian': self.guardian,
'village': self.village,
}

def years_months(self):
now = datetime.now().date()
ymonths = (now.year - self.dob.year) * 12
months = ymonths + (now.month - self.dob.month)
return (now.year - self.dob.year, months)

def age(self):
years, months = self.years_months()
if years > 3:
return str(years)
else:
return "%sm" % months

class Meta:
abstract = True
31 changes: 31 additions & 0 deletions malnutrition/models/facility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

class Facility(models.Model):
""" A generic model for a facility """
class Meta:
abstract = True
verbose_name_plural = "Facilities"

def __unicode__ (self):
return self.name

CLINIC_ROLE = 1
DISTRIB_ROLE = 2
ROLE_CHOICES = (
(CLINIC_ROLE, _('Clinic')),
(DISTRIB_ROLE, _('Distribution Point')),
)

name = models.CharField(max_length=255)
role = models.IntegerField(choices=ROLE_CHOICES, default=CLINIC_ROLE)
zone = models.ForeignKey("Zone", db_index=True)
codename = models.CharField(max_length=255,unique=True,db_index=True)
lon = models.FloatField(null=True,blank=True)
lat = models.FloatField(null=True,blank=True)

def get_dictionary(self):
return {
"name": self.name,
"codename": self.codename
}
22 changes: 22 additions & 0 deletions malnutrition/models/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _

from datetime import datetime

class MessageLog(models.Model):
""" This is the raw dirt message log, useful for some things """
mobile = models.CharField(max_length=255, db_index=True)
sent_by = models.ForeignKey(User, null=True)
text = models.TextField(max_length=255)
was_handled = models.BooleanField(default=False, db_index=True)
created_at = models.DateTimeField(db_index=True)

class Meta:
ordering = ("-created_at",)
abstract = True

def save(self, *args):
if not self.id:
self.created_at = datetime.now()
super(MessageLog, self).save(*args)
Loading

0 comments on commit afd7fce

Please sign in to comment.