Customize the Master Views¶
This quickly becomes a sprawling topic; there are a myriad of ways one might customize a master view class.
Unfortunately there is no “wizardry” to guide you through any of this, you are armed only with the documentation and source code.
For this tutorial we will limit to just a few relatively simple changes. It’s a simple app after all, not much else needs to be spiced up if you’re just trying to capture/track some data.
Adding hyperlinks for grid column cells¶
When you browse the /vehicles/ URL (and assuming you already
created at least one vehicle), you will notice the Action hyperlinks
on the right, but all of the data columns are just plain text.
The grid class considers the View action URL to be “canonical” for a row, and it’s often helpful to make one or more of the data columns render with a hyperlink using the View URL.
This is straightforward using
set_link() - for instance
we’ll do that for the VehicleView like so:
from wuttaweb.views import MasterView
from poser.db.model import Vehicle
class VehicleView(MasterView):
model_class = Vehicle
# ..<boilerplate elided>..
def configure_grid(self, grid):
super().configure_grid(grid)
# wrap these column values with view hyperlinks
grid.set_link("name")
#grid.set_link("make")
#grid.set_link("model")
And similarly we’ll go ahead and do this for the
MileageEntryView:
from wuttaweb.views import MasterView
from poser.db.model import MileageEntry
class MileageEntryView(MasterView):
model_class = MileageEntry
# ..<boilerplate elided>..
def configure_grid(self, grid):
super().configure_grid(grid)
# wrap these column values with view hyperlinks
grid.set_link("date")
grid.set_link("vehicle")
Note that we added the hyperlink logic for the vehicle column.
This may be confusing at first, since when you’re looking at a vehicle
name in the grid, and it’s a hyperlink, you might expect that to go to
the vehicle’s page. But no, these hyperlinks always/only use the View
action URL, whatever that happens to be for each row (mileage entry in
this case).
It’s of course possible to make a cell render with hyperlink which points somewhere else, but that requires more/different custom logic.
Fixing a foreign key reference form field¶
Not sure what else to call this “customization” but currently it is part of the game.
In our example it’s the Vehicle reference field when we need to create or edit a Mileage Entry record. The default form logic is not yet smart enough to handle such fields well. Indeed the behavior is effectively broken; hence we must “fix” it.
And really the fix here gives us a way to introduce more customization points, so there’s some silver lining. The first thing we must do is create a new “schema node type” class representing the “object reference” in question. In our case we need to reference a vehicle, so our node type will be based around that.
Create/edit the module at poser/web/forms/schema.py to define the
type class (you may need to make the forms dir and create empty
__init__.py for it as well):
from wuttaweb.forms.schema import ObjectRef
from poser.db.model import Vehicle
class VehicleRef(ObjectRef):
model_class = Vehicle
def sort_query(self, query):
# nb. could also do: query.order_by(Vehicle.make, Vehicle.model)
return query.order_by(Vehicle.name)
def get_object_url(self, vehicle):
return self.request.route_url("vehicles.view", uuid=vehicle.uuid)
As you can see this will control the ordering of vehicle records,
e.g. for use in a dropdown, and also specifies how to generate a URL
to view a given vehicle. See
ObjectRef for more about the
base schema type.
With that in place we can go back to edit the MileageEntryView
class, and tell it to use the custom type via
set_node(). This time we
must configure the form instead of the grid:
from poser.web.forms.schema import VehicleRef
class MileageEntryView(MasterView):
# ..<boilerplate elided>..
def configure_form(self, form):
super().configure_form(form)
# use custom node type for vehicle reference
form.set_node("vehicle", VehicleRef(self.request))
Now when you create or edit a mileage entry, things should look much better!
As a sanity check you can try to submit an empty form and it should not actually create the mileage entry, but give you some warnings instead. You should also be able to create a new record okay as long as you give it the required values.
Setting default column sort for a grid¶
Now if you add very many mileage entry records you will at some point notice that the grid is sortable, and then you may notice that it is not sorted by default. (For instance, after you first login to the app.)
That’s an easy fix via
sort_defaults,
e.g. like this:
class MileageEntryView(MasterView):
sort_defaults = ("date", "desc")
It’s generally a good idea to set default sort for all master views, so here’s the other one:
class VehicleView(MasterView):
sort_defaults = "name"
Note that you only have to specify column name unless you want a descending sort, as in the first example.
Also note that while in fact the default template theme supports multi-column sorting there is one caveat, you can’t specify a multi-column sort as the default. Single column only for the default sort.
Setting custom grid cell rendering¶
Quite often you may need to tweak how grid cell values are rendered. This is handled on a per-column basis of course.
Here we’ll do a couple of pretty basic things for the mileage entry grid, declaring one column should be rendered as “currency” (dollars) and another as a “quantity” (which adds thousand-separator commas):
class MileageEntryView(MasterView):
def configure_grid(self, grid):
super().configure_grid(grid)
# ..<previous link boilerplate elided>..
# show payment with dollar sign and 2 decimals
grid.set_renderer("fillup_payment", "currency")
# add thousands separator for odometer
grid.set_renderer("odometer", "quantity")
The set_renderer() method
has a few built-in renderers such as those shown here, but you can
also give it a callable which lets you render any way you like.
Note that how a grid “renders” a column’s values does not change their sorting behavior, since that is handled on the server side within the SQL database. (At least for typical scenarios.)
Unfortunately the grid rendering logic is only used by the grid, and will not affect the mileage entry form at all. So we make similar changes to it next.
Setting custom form field types¶
While we can just give the grid a “renderer callable” and thereby make column values appear however we like, it’s not that simple for form fields.
Each field is represented by multiple objects under the hood, which handle the data validation but also the readonly display. The latter can be customized in more than one way, but ultimately the field “widget” will do the rendering. (See also Forms.)
So one option is to set the widget explicitly, but you can also set the “type” for the field, which in turn will usually determine the widget.
We actually showed an example of this earlier, for the Vendor reference field using a custom node schema type.
This time around we’ll use
set_node() for the payment
and odometer fields, for an effect similar to what we did for the grid
above:
from wuttaweb.forms.schema import WuttaMoney, WuttaQuantity
class MileageEntryView(MasterView):
def configure_form(self, form):
super().configure_form(form)
# ..<previous vehicle field boilerplate elided>..
# use "money" type for better rendering
form.set_node("fillup_payment", WuttaMoney(self.request))
# use "quantity" type for better rendering
form.set_node("odometer", WuttaQuantity(self.request))
But while we’re here there’s another related tweak we should make.
There is a special notes widget we can use for such fields. This
will use a <textarea> element for editing, and also improves the
readonly field rendering:
class MileageEntryView(MasterView):
def configure_form(self, form):
super().configure_form(form)
# ..<previous boilerplate elided>..
# use special 'notes' widget for 'notes' field
form.set_widget("notes", "notes")
As with the grid set_renderer() call, this
set_widget() has “some”
(currently just the one for notes) built-in widgets but otherwise
it expects you to pass a full-on widget instance.
Now when viewing a single mileage entry record, the payment and odometer values should have better formatting. And when editing a record, the notes field should have a textarea.
Adding “rows” grid when viewing a record¶
You’ll notice when you view a mileage entry record, the Vehicle field has a hyperlink to view that record. This is courtesy of the “object reference node type” we setup earlier.
But when you view a vehicle record, the page is awfully sparse, with no indication of what mileage data we have collected for it.
The desired behavior will vary by use case, but here we’ll configure the vehicle view such that all related mileage entries are shown in a grid below the vehicle’s main form/fields.
The framework generally refers to this as a “rows grid” (as opposed to
the “main grid”) and the MasterView base class has various
features built-in for this pattern. It’s not all magic though, we
have to specify some details.
Many of the row grid details will be quite similar to the main grid
configuration we already did for the MileageEntry view, so we sort
of repeat ourselves a little here in that sense:
from poser.db.model import Vehicle, MileageEntry
class VehicleView(MasterView):
model_class = Vehicle
# ..<other boilerplate elided>..
has_rows = True
row_model_class = MileageEntry
# nb. omit vehicle column since that's the parent record
row_grid_columns = [
"date",
"fillup_gallons",
"fillup_payment",
"odometer",
]
# we again want reverse sort by date
rows_sort_defaults = ("date", "desc")
# must declare how to get the row grid data for a parent record
def get_row_grid_data(self, vehicle):
session = self.Session()
# nb. we do *not* sort this query, since user can click
# column headers to control sorting separately. (in certain
# other scenarios e.g. where data is not a query, you may
# want to pre-sort the row data.)
return session.query(MileageEntry).filter(MileageEntry.vehicle == vehicle)
# also must declare how to get the parent record for a given row
def get_row_parent(self, mileage_entry):
return mileage_entry.vehicle
# configuring a row grid works the same, just a different method name
def configure_row_grid(self, grid):
super().configure_row_grid(grid)
# add view hyperlink for date column
grid.set_link("date")
# show payment with dollar sign and 2 decimals
grid.set_renderer("fillup_payment", "currency")
# add thousands separator for odometer
grid.set_renderer("odometer", "quantity")
# add 'view' action, if user is allowed access
if self.request.has_perm("mileage_entries.view"):
grid.add_action("view", icon="eye", url=self.get_row_action_url_view)
# view URL generator for a given row
def get_row_action_url_view(self, mileage_entry, i):
return self.request.route_url("mileage_entries.view", uuid=mileage_entry.uuid)
Note that some of the special class attributes above are prefixed with
row_ but some are prefixed with rows_ - unless/until those are
aligned, you just have to watch out for that.
FYI, it’s possible to setup CRUD for these “row records” as well, within the vehicle master view. But in this case we already have full CRUD for the mileage entries, therefore we setup the view URL logic to point to the mileage entries master view.
With the above in place, viewing a vehicle record should become more interesting. The row grid works the same as the main grid, in terms of sorting, filtering (etc.) from user’s perspective.
You should now have easy navigation from a vehicle to a particular mileage entry, and vice versa.
Here are docs for all the row-related things we just touched:
Summary¶
Just to wrap things up, we’ve touched on several common customization needs for the master views. But of course this barely scratches the surface. (We didn’t even touch any templates or JS etc.)
Unfortunately this area needs a lot more documentation than currently exists. For now the WuttaWeb package API docs may be the most helpful. But regardless WuttaWeb is where you’ll find everything.
However this tutorial is not over just yet, so keep reading.