Table Tutorial ================================= Here you can find out how to create tables with sprox, including infinitely scrolling tables provided by Dojo. Many of these examples will provide server code based on TurboGears2, but sprox will work with any wsgi application you have ToscaWidgets mounted in. We will assume the reader is somewhat versed in TurboGears2's :class:`tg.controllers.RestController`. Establishing the Model Definition ----------------------------------- Let us first assume the following model for this demonstration.:: from sqlalchemy import Column, Integer, String, Date, Text, ForeignKey, Table from sqlalchemy.orm import relation from moviedemo.model import DeclarativeBase, metadata movie_directors_table = Table('movie_directors', metadata, Column('movie_id', Integer, ForeignKey('movies.movie_id'), primary_key = True), Column('director_id', Integer, ForeignKey('directors.director_id'), primary_key = True)) class Genre(DeclarativeBase): __tablename__ = "genres" genre_id = Column(Integer, primary_key=True) name = Column(String(100)) class Movie(DeclarativeBase): __tablename__ = "movies" movie_id = Column(Integer, primary_key=True) title = Column(String(100), nullable=False) description = Column(Text, nullable=True) genre_id = Column(Integer, ForeignKey('genres.genre_id')) genre = relation('Genre', backref='movies') release_date = Column(Date, nullable=True) class Director(DeclarativeBase): __tablename__ = "directors" movie_id = Column(Integer, primary_key=True) title = Column(String(100), nullable=False) movies = relation(Movie, secondary_join=movie_directors_table, backref="directors") Basic Table Definition ----------------------- Here, we create a :class:`sprox.tablebase.TableBase` for the movie.:: from tg.controllers import RestController, redirect from moviedemo.model import DBSession, Movie, Genre, Director from sprox.tablebase import TableBase class MovieTable(TableBase): __model__ = Movie movie_table = MovieTable(DBSession); class SproxMovieController(RestController): @expose('moviedemo.templates.sproxdemo.movies.get_all') def get_all(self): tmpl_context.widget = movie_table return dict() And some template code::
 

Movie Listing

New Movie ${tmpl_context.widget()}
Which produces something like this: .. image:: images/simple_table.png Displaying Actual Data ----------------------------------- Now, we have a form, but there are no entries for that form. Lets add a :class:`sprox.fillerbase.TableFiller` to provide data for the form.:: from sprox.fillerbase import TableFiller class MovieTableFiller(TableFiller): __model__ = Movie movie_filler = MovieTableFiller(DBSession) Then we add that to our controller method:: @expose('moviedemo.templates.sproxdemo.movies.get_all') def get_all(self): tmpl_context.widget = movie_table value = movie_filler.get_value() return dict(value=value) Our template's call of the widget changes like this:: ${tmpl_context.widget(value=value)} Resulting in the data being shown. .. image:: images/table_values.png You may have noticed that the genre, and directors fields are already populated. Sprox pulls data from the relations you set up in your model definition, and displays them according to a default set of fieldnames as provided by the :class:`sprox.viewbase.ViewBase`. This may be altered by providing an additional __possible_field_names__ modifier. Removing unwanted fields ----------------------------------- This table is not really pleasing to an end-user because of the movie_id and genre_id fields so let's omit them. Our table definition becomes:: class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id'] which looks like this: .. image:: images/table_omit.png You can modify the filler to omit this data also, but it is not necessary. The view will only view the fields specified by your TableBase class. :class:`sprox.tablebase.TableBase` provides a number of modifiers that allow you to define how the table is displayed. Removing the actions field ----------------------------------- Often times, you will want to omit the "actions" field from the table. It may be that you simply want to display the data, with no other allowable actions to perform. We can modify our table to do just that:: class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] Custom field data display ----------------------------------- Sometimes it makes sense to provide your own data. What would happen if we wanted to display the Director's names as links to another page that lists the movies which they directed? Let's see how that would look:: class MovieTableFiller(TableFiller): __model__ = Movie def directors(self, obj): directors = ', '.join([''+d.name+'' for d in obj.directors]) return directors.join(('
', '
')) class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['directors'] And now you can see the resulting directors are listed as hrefs: .. image:: images/table_directors.png There are two new features from 0.6 in this example. The first is that you may specify any function in your filler, and if that function matches the name of a field in the Fillers __entity__ it will use that function to produce a string output for filling of that row of data. The function takes a reference to the parent object, so if you are overriding a relation (as we are in this example) you have full access to the object's scope. The second new feature is the modifier __xml_fields__ which tells the simple table rendering ToscaWidget not to escape the html tags. This allows the links to "work" in our example. Adding a new Field ----------------------------------- Sometimes you want to augment the field entries with one of your own. Let's add a "similar movies" column that displays movies of the same genre. First, we must modify the table view to include our new column.:: class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['directors'] __add_fields__ = {'related_movies':None} movie_table = MovieTable(DBSession); We also need to modify the filler to include the new column, and define a function for it's display.:: class MovieTableFiller(TableFiller): __model__ = Movie __add_fields__ = {'related_movies':None} def directors(self, obj): directors = ', '.join([''+d.name+'' for d in obj.directors]) return directors.join(('
', '
')) def related_movies(self, obj): return ', '.join([movie.title for movie in obj.genre.movies if movie!=obj]) movie_filler = MovieTableFiller(DBSession) The resultant table should look something like this: .. image:: images/table_new_column.png Changing the TableFiller's query ---------------------------------- It is likely at some point that you will want to override sprox's table querying mechanism. Maybe you want to provide pagination or limit the results to a specific subset of records. Let's add a new Controller for the directors that print's their name and displays the movies they directed. Here's the new controller code:: class SproxDirectorController(RestController): @expose('moviedemo.templates.sproxdemo.directors.get_one') def get_one(self, director_id): tmpl_context.widget = director_movie_table director = DBSession.query(Director).get(director_id) value = director_movie_filler.get_value(director=director) return dict(director=director, value=value) Our director template could look something like this::
 

${director.name}

Movies Directed

${tmpl_context.widget(value=value)}
And our director-specific TableFiller:: class DirectorMovieTableFiller(TableFiller): __model__ = Movie __add_fields__ = {'related_movies':None} def directors(self, obj, **kw): director = kw['director'] directors = ', '.join([''+d.name+'' for d in obj.directors if d!=director]) return directors.join(('
', '
')) def related_movies(self, obj): return ', '.join([movie.title for movie in obj.genre.movies if movie!=obj]) def _do_get_provider_count_and_objs(self, director=None, **kw): movies = DBSession.query(Movie).filter(Movie.directors.contains(director)).all() return len(movies), movies director_movie_filler = DirectorMovieTableFiller(DBSession) The first difference you may notice is that we have added a _do_get_provider_count_and_objs method. This method defines how the query should be performed for the given filler. In this case, we are filtering only on Movies which have the indicated director. The director value comes in from your get_value call inside the controller. The function returns both a length and a list of objects. The length is the length of all objects that may be queried, the list of objects is just those the user wants for the given context. This is particularly useful when designing paginated tables. You may also notice that we have changed the directors method to include the director in the call. Sprox will also pass variables in from the get_value call to your field methods if you specify parameters for those variables in your method call. Our TableBase code is largely unchanged.:: class DirectorMovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['directors'] __add_fields__ = {'related_movies':None} director_movie_table = DirectorMovieTable(DBSession); And the resulting page. .. image:: images/table_director.png Changing the Column Header Names ---------------------------------- While the column names are functional, they aren't pretty, and in the case of the director page, it really should say "other directors". Let's use the __headers__ modifier and change that.:: class DirectorMovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['other directors'] __add_fields__ = {'related_movies':None} __headers__ = {'directors': 'other directors'} And the compulsory screenshot: .. image:: images/table_header.png Adding Dojo to the mix ------------------------ Dojo is a JavaScript library that sprox uses to create tables that are infinitely scrolling. This is especially handy if you don't want to go through the trouble of writing pagination code. The first thing we need to do is install tw.dojo, which is a toscawidget wrapper for the library:: easy_install tw.dojo Next we modify our Table code to utilize the sprox dojo filler and table classes:: from sprox.dojo.tablebase import DojoTableBase as TableBase from sprox.dojo.fillerbase import DojoTableFiller as TableFiller class MovieTableFiller(TableFiller): __model__ = Movie __add_fields__ = {'related_movies':None} def directors(self, obj): directors = ', '.join([''+d.name+'' for d in obj.directors]) return directors.join(('
', '
')) def related_movies(self, obj): return ', '.join([movie.title for movie in obj.genre.movies if movie!=obj]) movie_filler = MovieTableFiller(DBSession) class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['directors'] __add_fields__ = {'related_movies':None} __url__ = "/movies.json" movie_table = MovieTable(DBSession) Now if you take a look at our movie page you will see that there is a table, but no data: .. image:: images/table_dojo_nodata.png This is because dojo has a different method of obtaining the table data. It performs an Ajax call back to the server to retrieve a stream of data. We need to change our controller method to publish this stream:: class SproxDirectorController(RestController): @expose('moviedemo.templates.sproxdemo.directors.get_one') def get_one(self, director_id): tmpl_context.widget = director_movie_table director = DBSession.query(Director).get(director_id) value = director_movie_filler.get_value(director=director) return dict(director=director, value=value) Once we fix this we get our data back: .. image:: images/table_dojo_data.png Changing column widths -------------------------- Sprox sets all column widths to 10em by default. This will probably need to be modified in order for your data to look right. Here is how we do that:: class MovieTable(TableBase): __model__ = Movie __omit_fields__ = ['movie_id', 'genre_id', '__actions__'] __xml_fields__ = ['directors'] __add_fields__ = {'related_movies':None} __column_widths__ = {'description':"30em", 'release_date':"7em", 'genre':"4em", } __url__ = "/movies.json" And here is how it looks after modification: .. image:: images/table_column_widths.png To Summarize ------------- TableBase and TableFiller provide an easy path for display of data to a webpage. They can be heavily customized, especially with the advent of field_methods in 0.6, which allow you to provide a python function for field data display. tw.dojo gives TableFiller and TableBase with the capability to have an ajaxy interface with infinite scrolling. If you'd like to review the code used to create this tutorial, please:: svn co http://pythontutorials.googlecode.com/svn/tutorials/moviedemo/trunk moviedemo