Rum

Extensible CRUD interface for your model objects

Welcome to Rum's demo!

NEW! Now with i18n support, take a look at the demo in Spanish or English or configure your browser to send the appropiate Accept-Language headers. (This page you're reading hasn't been translated though)

Thanks for taking a look. Funtionality is quite limited at the moment but hey, development began on the 26th of June... ;)
(I was busy building toscawidgets.org and writting docs before that)

Go ahead, play around. Model objects are listed at your left. List them, edit them, delete them, create new ones...

How to try it out in your system

Install software

First download virtualenv to create an isolated Python environment.

        $ python virtualenv.py RumDemo
        $ cd RumDemo
        $ source bin/activate
        (In Windows you need to execute bin/activate.bat)
        $ easy_install -f http://toscawidgets.org/download SQLAlchemy==0.4.6 Paste WebError RumAlchemy tw.rum
        

Those lines should get you all the software you need. Note that we "pin" SQLAlchemy 0.4.6 since, although 0.5 can be introspected, etc.. there is some bug that drives the app into infinite recursion with model inheritance which I need to investigate and report.
WebError is helpful for debugging and Paste seems to have it's metadata messed up in PyPI at the moment so it can't be pulled automatically.

Get demo source code

You can copy and paste the code below, making sure you remove the 'default_directories' hardcoded path, or you can download a clean version from here

The code this app runs on

Yes, this single file is all there is, well, plus a custom template I'm using to generate this page you're seeing and a .wsgi file so mod_wsgi can serve the app which also does a little hack to inject a Google analytics widget so I can know you've been here ;) You don't need those though.
import sys
import logging
import datetime
from optparse import OptionParser

from sqlalchemy import Column, ForeignKey, Table, PrimaryKeyConstraint
from sqlalchemy.types import *
from sqlalchemy.orm import relation
from sqlalchemy.ext.declarative import declarative_base

from rumalchemy.tests.model2 import *

from rum import FieldFactory, _, N_, fields

#
# Metadata declaration
#
N_("person"); N_("persons")
FieldFactory.fields(Person, (
    fields.Unicode('name', label=_('Name'), required=True),
    fields.Integer('age', label=_('Age'), range=(0,150)),
    fields.UnicodeText('notes', label=_("Notes")),
    "rentals",
    "type",
    "version",
    ))

N_("actor"); N_("actors")
FieldFactory.fields(Actor, (
    fields.Unicode('name', label=_('Name'), required=True),
    fields.Integer('age', label=_('Age'), range=(0,150)),
    fields.Integer('oscars_won', label=_("# Oscars won"), range=(0,10)),
    fields.UnicodeText('notes', label=_("Notes")),
    "rentals",
    "movies",
    "type",
    "version",
    ))

N_("director"); N_("directors")
FieldFactory.fields(Director, (
    fields.Unicode('name', label=_('Name'), required=True),
    fields.Integer('age', label=_('Age'), range=(0,150)),
    fields.Integer('chairs_broken', label=_("# Chairs broken"), range=(0,50)),
    fields.UnicodeText('notes', label=_("Notes")),
    "rentals",
    "movies",
    "type",
    "version",
    ))

N_("genre"); N_("genres")
FieldFactory.fields(Genre, (
    fields.Unicode('name', label=_('Name'), required=True),
    "movies",
    ))

N_("movie"); N_("movies")
FieldFactory.fields(Movie, (
    fields.Unicode('title', label=_('Title'), required=True),
    "genre",
    "director",
    "actors",
    fields.JPEGImage('poster', label=_('Poster in JPEG format')),
    fields.Date('filmed_on', label=_('Filmed on')),
    fields.HTMLText("synopsis", label=_('Synopsis')),
    ))

N_("rental"); N_("rentals")
FieldFactory.fields(Rental, (
    "person",
    "movie",
    fields.DateTime('date', label=_('Rental date')),
    fields.DateTime('due_date', label=_('Due date')),
    fields.Boolean('is_overtime', label=_('Is overtime?'), read_only=True),
    ))

N_("movieInGenre"); N_("moviesInGenres")
FieldFactory.fields(MovieInGenre, (
    fields.Unicode('title', label=_('Title'), required=True),
    fields.Unicode("genre_name", label=_('Genre name'), required=True),
    fields.JPEGImage('poster', label=_('Poster in JPEG format')),
    fields.Date('filmed_on', label=_('Filmed on')),
    fields.HTMLText("synopsis", label=_('Synopsis')),
    ))

#
# A parser for command line options
#
parser = OptionParser()
parser.add_option('', '--dburl',
                  dest='url',
                  help='SQLAlchemy database uri (eg: postgres:///somedatabase)',
                  default='sqlite:///rum_demo.db')
parser.add_option('-d', '--debug',
                  dest='debug',
                  help='Turn on debug mode',
                  default=False,
                  action='store_true')

#
# Makes the app
#
def load_app(url, debug=False):
    from pkg_resources import require
    require('RumAlchemy', 'tw.rum')
    from rum import RumApp
    models = [Person, Genre, Actor, Director, Movie, Rental, MovieInGenre]
    return RumApp({
        'debug': debug,
        'rum.repositoryfactory': {
            'use': 'sqlalchemy',
            'models': models,
            'sqlalchemy.url': url,
            'session.transactional': True,
        },
        'templating': {
            'search_path': ['/home/toscawidgets/RumDemo/templates'],
        },
        'rum.viewfactory': {
            'use': 'toscawidgets',
        },
        'rum.translator': {
            'use': 'default',
            'locales': ['en', 'es'],
        },
    })

#
# Main calling point
#
def main(argv=None):
    from sqlalchemy import create_engine
    from paste.deploy import loadserver
    logging.basicConfig(level=logging.INFO, stream=sys.stderr)
    opts, args = parser.parse_args(argv)
    Model.metadata.create_all(bind=create_engine(opts.url))
    app = load_app(opts.url, opts.debug)
    server = loadserver('egg:Paste#http')
    try:
        server(app)
    except (KeyboardInterrupt, SystemExit):
        print "Bye!"

if __name__ == '__main__':
    sys.exit(main(sys.argv))

Some cool things you might overlook

  • WSGI compliant and "non-singleton", ie: multiple Rum instances can run in the same process safely each one of them configured differently. It will be trivial to integrate in a Pylons or TurboGears2 and easy to integrate in Django, web.py, etc... Can also be used standalone (like if this demo didn't convince you... ;)
  • Ridiculously extensible: All templates can be overrided, controllers can be overrided, data providers (repositories in Rum parlance) can be overrided too to control how your models are persisted, displayed and manipulated. If you're feeling brave you can also add rules to many methods (which are designed to be extended this way) with PEAK-Rules although an API that hides this will be implemented for the faint of heart.
    This backed supports SqlAlchemy models but a SQLObject handler will be implemented too to ease migration for TG1 users to TG2.
    A ZODB backend is also planned and a CouchDB one might also be implemented.
  • Very loosely coupled. Views can be reused among data providers, they never interface SA (or whatever) directly. Controllers talk to the providers using well-defined interfaces so they don't need to change when the data provider changes (and viceversa).
  • Uses ToscaWidgets so widgets can be easily customized. You can also avoid using ToscaWidgets altogether if you really hate it ;)
  • Model inheritance is supported. Take a look at the persons listing where you can see Persons, Actors and Directors. The urls for the actions of each item are generated properly too.
  • RESTful semantics bottom-up. updates are done through PUT (faked with a hidden field for browser form submissions), deletes through DELETE, etc.. No state whatsoever is kept on the webapp in the form of sessions and the like.
  • Every method can return JSON. Just append a .json suffix to the end of the URL. or craft a request that lists application/json in it's Accepts header.
  • Metadata for each model can be queried at the _meta method. See example. Of course, this metadata can also be requested in JSON format to generate the UI on the client.
  • Documenation will be great this time, I promise ;). Preeliminary API docs are here.
  • Many more things and much more to come... use the source Luke! :)

Feedback

Feedback will be greatly appreciated :) Development is going fast now and there's no legacy API to keep compatible with so your input can have a great impact on the direction development takes. I'm also in the look for fellow developers so if you're interested in joining forces by all means drop me a note!.

We've created a mailing list to discuss Rum. Please join us at rum-discuss.

Some metrics

Although I personally find them pretty meaningless (specially the test coverage since I know I'm not yet tesing many things but the coverage is still ridiculously high) you might find them interesting.

Rum

Test coverage

        Name                   Stmts   Exec  Cover   Missing
        ----------------------------------------------------
        rum                       30     30   100%   
        rum.component             66     66   100%   
        rum.controller           225    224    99%   364
        rum.exceptions            11     11   100%   
        rum.fields                63     63   100%   
        rum.genericfunctions       3      3   100%   
        rum.interfaces            54     54   100%   
        rum.json                  11      4    36%   6-8, 12-15
        rum.middleware            32     31    96%   36
        rum.repository           119    118    99%   231
        rum.router               170    170   100%   
        rum.templating             9      9   100%   
        rum.util                  22     22   100%   
        rum.view                  15     15   100%   
        rum.widgets                3      3   100%   
        rum.wsgiapp              124    124   100%   
        rum.wsgiutil              14     14   100%   
        ----------------------------------------------------
        TOTAL                    971    961    98%   
        ----------------------------------------------------------------------
        Ran 140 tests in 27.017s
        

misc stats

           lines    code     doc comment   blank  file
         ------- ------- ------- ------- -------  -----------------------------------
              60      49       0       5       6  rum/__init__.py
             125      71      37       5      12  rum/component.py
             416     304      23      20      69  rum/controller.py
              15      11       0       0       4  rum/exceptions.py
             399      65     283      18      33  rum/fields.py
               9       8       0       0       1  rum/genericfunctions.py
             262      69     142       5      46  rum/interfaces.py
              15      13       0       0       2  rum/json.py
              48      35       0       7       6  rum/middleware.py
             236     139      48       7      42  rum/repository.py
             238     183       1      15      39  rum/router.py
              10       9       0       0       1  rum/templating.py
               0       0       0       0       0  rum/tests/__init__.py *
              46      34       0       0      12  rum/tests/model.py *
              95      73       0       3      19  rum/tests/test_component.py *
             303     248       4       5      46  rum/tests/test_controller.py *
              97      66       0      13      18  rum/tests/test_repository.py *
             699     605       0       3      91  rum/tests/test_router.py *
              42      33       0       0       9  rum/tests/test_view.py *
              40      28       4       0       8  rum/tests/test_wsgiapp.py *
              38      27       6       1       4  rum/testutil.py
              78      24      47       3       4  rum/util.py
              52      16      26       2       8  rum/view.py
              23       7       0      14       2  rum/widgets/__init__.py
             205     154      18       2      31  rum/wsgiapp.py
              18      14       0       0       4  rum/wsgiutil.py
            3569    2285     639     128     517  [total]
        Test code: 1087  other code: 1198  ratio: 1:0.9 code to tests

        2285 lines of code, 26 modules, 3 packages.
        

RumAlchemy

Test coverage

        Name                           Stmts   Exec  Cover   Missing
        ------------------------------------------------------------
        rumalchemy                         3      3   100%   
        rumalchemy.json                    3      3   100%   
        rumalchemy.repository            116     74    63%   76-78, 121-122, 126-130, 134-136, 143-146, 148-152, 155-158, 166, 172, 178, 185-186, 194-199, 202-206, 209, 212, 230
        rumalchemy.util                   10      7    70%   13-15
        rumalchemy.validators             28     15    53%   19, 23, 26-33, 35-41
        rumalchemy.viewfactory            88     78    88%   76, 104, 108, 118, 140-141, 162-163, 195-196
        rumalchemy.widgets                45     36    80%   17-26
        rumalchemy.widgets.grid           19     14    73%   22-23, 26-29
        rumalchemy.widgets.templates       0      0   100%   
        ------------------------------------------------------------
        TOTAL                            312    230    73%   
        ----------------------------------------------------------------------
        Ran 9 tests in 12.054s
        

misc stats

           lines    code     doc comment   blank  file
         ------- ------- ------- ------- -------  -----------------------------------
               4       3       0       1       0  rumalchemy/__init__.py
               4       3       0       0       1  rumalchemy/json.py
             232     153      35       8      36  rumalchemy/repository.py
               0       0       0       0       0  rumalchemy/tests/__init__.py *
              88      62       0       5      21  rumalchemy/tests/model.py *
              38      30       0       1       7  rumalchemy/tests/test_resource_introspection.py *
              54      43       0       1      10  rumalchemy/tests/test_sawidget_factory.py *
              15      11       0       1       3  rumalchemy/util.py
              42      32       0       3       7  rumalchemy/validators.py
             198     137       4      23      34  rumalchemy/viewfactory.py
              99      47      34       3      15  rumalchemy/widgets/__init__.py
              34      29       0       1       4  rumalchemy/widgets/grid.py
               0       0       0       0       0  rumalchemy/widgets/templates/__init__.py
             808     550      73      47     138  [total]
        Test code: 135  other code: 415  ratio: 1:0.3 code to tests