Table of Contents
This project is a simple but real-life implementation of statistics report generator that is written by me for my friend in our local clinic. He works at the UltraSonoGraphy department hence the name of package (UZI is an abbreviation for USG in Russian).
I decided to donate this project to public because it was very hard for me to find out any public documentation about how to make a web-based application using Genshi, SQLAlchemy/Elixir and ToscaWidgets. The TurboGears 1.0 documentation talked primarily about Kid, SQLObject and TG Widgets.
This software is published under terms of GPL3+ license.
Here is what examples of usage you can find here
- Figure in one cell must be equal to sum of figures in set of other cells
- Figure in one cell must be not less than figure in other cell
First of all I needed to make a page where user can type in daily results. So date must be selected, doctor who did the examinations must be selected and then a set of values each corresponding to some report field. After some thought I decided to use radiobuttons list. It takes more space, but saves time (all data is entered by one person in batch). So here is what I did:
class NewResultForm(TableForm):
action='/enter_data_confirm'
def __init__(self, **dt):
HiddenField('id', self, validator = Int)
RadioButtonList('doctor',self,
label_text = u("Doctor"),
options = [(d.id,d) for d in
Doctor.query.filter(Doctor.visible==1).all()],
validator = Int,
)
CalendarDatePicker('date',self,label_text = u("Date"),
date_format='%Y-%m-%d',
button_text=u('Change'),calendar_lang='ru',
validator=Regex("^20\d\d-(0\d|1[012])(-([012]\d|3[01])|)$"))
chain1,chain2,chain3,chain4,chain5=[],[],[],[],[]
for o in Organ.query.all():
TextField(':doc:`/organ`%i'%o.id, self, label_text = o.name,
size=2, validator = Int)
SubmitButton('submit', self, default = u('Submit'))
TableForm.__init__(self, **dt)
And here is how it looks like:
Then I needed to add some custom validators - that is - if we have patient - then we have at least one examination, number of patients came from different subdivisions must be equal to total number of patients, number of all examinations done by that day must be equal to sum of all examinations by diffrent organs. So I wrote couple of my own validators, one for ‘sums match’ and other for ‘one is not less than another’.
I’ll give the modified form code later, for now just how validator itself looks like:
class FieldsSumMatch(FieldsMatch):
""" Validates that value of the first field in the given sequence
matches sum of values of other fields in sequence with
the precision of 10^-4. If given less than two fields always
raises an Exception. If given only two fields behaves as
primitive version of FieldsMatch validator.
"""
def validate_python(self, field_dict, state):
try: val,sum= float(field_dict[self.field_names[0]]),0.0
except: raise Invalid(u('Total sum is not specified!'),'',
state,error_dict={self.field_names[0]:u('Enter value.')})
try:
errors={}
for name in self.field_names[1:]:
try:
sum+=float(field_dict[name])
errors[name]=u('Sum doesn\'t match.')
except: pass
errors[self.field_names[0]]=u('Doesn\'t match %s.'%int(sum))
diff=val-sum
assert diff*diff<0.00000001
except:
raise Invalid(u('Sum doesn\'t match: %s != %s'%(sum,val)),
'',state,error_dict=errors)
And here is how it looks when it triggered:
Here we have specified that we done 4 examinations, but in the details only three are specified (2 breasts and 1 ‘soft tissues’). So validator triggers.
Another validator checks that value of first field is equal or greater than value in another field:
class FirstNotGreaterOrBothEmpty(FieldsMatch):
""" Validates that value first given field value is less
or equal to second field value.
Allows both fields to be empty
"""
def validate_python(self, field_dict, state):
errors={}
try: val1 = float(field_dict[self.field_names[0]])
except: val1=0
try: val2 = float(field_dict[self.field_names[1]])
except: val2=0
if val2 and not val1: errors[self.field_names[0]]=u('Value too small.')
if val1>val2:
errors.update({
self.field_names[1]:u('Value too small.'),
self.field_names[0]:u('Value too big.'),})
if errors:
raise Invalid('','',state,error_dict=errors)
And here is how it looks when we made a mistake in entered data:
Here we specified that there were fewer examinations than total number of patients. That couldn’t happen either.
And finally, as I promised, here is how I plugged these validators into the form. When creating database with report fields I put down a set of identifiers:
Number of examinations 1
Number of patients 2
Ambulance patients 3
Stationary patients 3
RECPT. patients 6
RECPT. examinations 5
RECPT. points 4
Liver 0
Genitalia 0
Pregnacy 0
Heart 0
Breast 0
Soft tissues 0
And the logic was: sum of all fields of type ‘0’ must be equal to value of field ‘1’, field ‘1’ must be not less than field ‘2’, field ‘2’ must be equal to sum of fields ‘3’ and ‘6’ and so on. These identifiers are stored in the db and I use them to create an ad-hoc validation logic:
class NewResultForm(TableForm):
action='/enter_data_confirm'
def __init__(self, **dt):
HiddenField('id', self, validator = Int)
RadioButtonList('doctor',self,
label_text = u("Doctor"),
options = [(d.id,d) for d in
Doctor.query.filter(Doctor.visible==1).all()],
validator = Int,
)
CalendarDatePicker('date',self,label_text = u("Date"),
date_format='%Y-%m-%d',button_text=u('Change'),
calendar_lang='ru',
validator=Regex("^20\d\d-(0\d|1[012])(-([012]\d|3[01])|)$"))
chain1,chain2,chain3,chain4,chain5=[],[],[],[],[]
for o in Organ.query.all():
fname=':doc:`/organ`%i'%o.id
TextField(fname, self, label_text = o.name, size=2, validator = Int)
#1=results; 2=patients; 3=res>pati; 4=Pres>Ppati; 5=PUE>Pres
if o.type==0:
chain1.append(fname)
elif o.type==1:
chain1.insert(0,fname)
chain3.append(fname)
elif o.type==2:
chain2.insert(0,fname)
chain3.insert(0,fname)
elif o.type==3:
chain2.append(fname)
elif o.type==4:
chain5.append(fname)
elif o.type==5:
chain4.append(fname)
chain5.insert(0,fname)
elif o.type==6:
chain2.append(fname)
chain4.insert(0,fname)
#1>=2 #2=3+3+6 #5>=6 #4>=5 #1=sum(0)
self.validator = TGSchema(chained_validators = [
DoctorValidator('doctor'), FieldsSumMatch(*chain1),
FieldsSumMatch(*chain2), FirstNotGreaterOrBothEmpty(*chain3),
FirstNotGreaterOrBothEmpty(*chain4),
FirstNotGreaterOrBothEmpty(*chain5) ], if_key_missing = 0, )
SubmitButton('submit', self, default = u('Submit'))
TableForm.__init__(self, **dt)
Here I create lists of fields so that ‘main field’ is first in this list and fields that are going to be summed are the rest of the list. I know, that’s not pretty. But at the time (and even now) it was quick and not bad solution. After all, may be you will have static fields names so you’ll just make hardwired list of identifiers like FieldsSumMatch(‘this_is_sum’, ‘to_be_summed_1’, ‘to_be_summed_2’).
At some point a need arised to make cell contents clickable. For instance, allow edition of report field or Doctor name or allow to re-edit the once entered data.
For that I wrote couple of functions:
def make_edit_link(obj,edit_url):
name=obj.__str__()
if not obj.visible:
name+=u('<font color="#ff0000"> '+
'(disabled%s)</font>'%{True:'',False:'о'}['d' in edit_url])
return make_link(name,edit_url,{'id':obj.id},anchor='#edit')
def make_link(name, url, args={}, args2={}, anchor=''):
args.update(args2)
return genshi.XML(u"<a href='%s#%s'>%s</a>"%
(tg.url(url,args),anchor,name))
First one is project-specific - it alters cell contents depending on if corresponding object disabled for reports or not. Scond one (make_link) is what you may be looking for - it is a backend for clickable table cell. WARNING: make_link function relies onto __repr__ property of the object to produce some readable content. You MUST redefine it because by default it triggers invalid XML error. So I used these functions to make editable <td/>s (since according to CSS they are not rendered as links - I marked clickable fields with green):
@expose(template="uzi.templates.db_edit")
@identity.require(identity.in_group("admin"))
def doctors(self, **dt):
return self.view_objs(Doctor, dt.get('id',None), u('Doctors'), [
(u('Doctor'),lambda x: make_edit_link(x,'/doctors')),
(u('Days in single report'),'datetype'),
(u('Order'),'order'),],doctor_edit_form,)
def view_objs(self, Obj, obj, pagetitle, fields, obj_edit_form):
if obj: obj=Obj.get_by(id=obj)
objs=Obj.query
if Obj==Organ: objs=objs.filter(Organ.type==0)
return dict(
view_form=DataGrid(fields=fields),
view_data=objs.all(),
edit_form=obj_edit_form,
edit_data=obj,
pagetitle=pagetitle,
)
... and editable <th/>s ...
for doc,date,id in docs:
title=make_link(u'%s %s'%(doc.__str__(),
str(date)[:{1:10,30:7}[doc.datetype]]),
"/enter_data_form", {'edit_id':id})
fields.append((title,str(id)))
return dict(
results_query_form=ResultsQueryForm(validator=TGSchema),
report_form=DataGrid(fields=fields),
results=data,
query=dt,
)
While working on a project I found these two things to be incredibly helpful:
For completeness, here is the whole project. It’s not too large - about 500 lines written by me and some more generated by the TurboGears quickstart script. You can download it as source tarball or egg.