Sample Application Using Genshi And Elixir

Description

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.

Features

Here is what examples of usage you can find here

  1. Input forms
  2. Table reports
  3. Clickable, dynamically-generated table cells - both <th/> and <td/>
  4. Standard validators
  5. Custom validators
  • 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
  1. Printer-friendly report page

Screenshots and code

Data entry

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:

_images/uzi_data_entry.png

Validation

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’.

‘Sum matches’ Validator

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 Exception:
           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 Exception:
                   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:

_images/uzi_sum_validator.png

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.

‘Not less’ validator

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 lesser 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 Exception:
           val1 = 0
       try:
           val2 = float(field_dict[self.field_names[1]])
       except Exception:
           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:

_images/uzi_sum_not_less_validator.png

Here we specified that there were fewer examinations than total number of patients. That couldn’t happen either.

Embedding validators into form

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’).

Clickable table cells

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 = unicode(obj)
   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, link, args={}, args2={}, anchor=''):
   args.update(args2)
   return Markup(u"<a href='%s#%s'>%s</a>" % (url(link, 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 markup 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'), 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 is 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)
_images/uzi_clickable_td.png

... and editable <th/>s ...

for doc, date, id in docs:
    title = make_link(u'%s %s' % (unicode(doc),
        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)
_images/uzi_clickable_th.png

Helpful sources

While working on a project I found these two things to be incredibly helpful:

  1. Toscawidgets’ Browser. When you got toscawidgets properly intalled it is accessible by simply running twbrowser at the command prompt. ‘Properly’ here means that I had some problems starting my work with toscawidgets until I realized that they are distributed in several packages so I need to install more.
  2. Reading the source code (of course!) of toscawidgets and validators (which apparently comes from formencode package).

Download

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. Use the bootstrap-uzi command to fill the database tables with some data.

Author

If you got any questions regarding this application you may ask me by email to snakeru mailbox hosted at GMail.

Alexey Nezhdanov