[[TOC]]
== Admin Tool ==

modu comes with a configurable Resource implementation that provides an administrative interface for editing database data. Unlike the admin tools included with many other web frameworks, the modu admin requires a little more time up front for configuration, but in exchange provides a great deal of flexibility and customization.

The fundamental building-block of the admin is called an item definition, or 'itemdef'. An itemdef is a Python class that, like FormNodes, is used as a pseudo-DSL, to allow for an expressive configuration syntax. Once an itemdef is created, it can be used to generate forms for its various view modes.

The basic structure of the admin tool is split between 'list' and 'detail' modes. In list mode, users paginate through the rows of the table being managed, whereas detail view allows actual editing of the row in question (assuming you have the proper permissions).

=== What is an Itemdef? ===

An itemdef is a python object that can take a given Storable object and return a modu FormNode object with customized validation and submission functions already applied. The default submission process applies the modified data back to the Storable object and saves it to the database, while providing a number of optional callbacks, custom datatypes, and other conveniences that make building a custom admin tool a fairly straightforward process.

The conventional way to create itemdefs is to place the definition for each one inside its own Python module file. Here's an minimal example for a single-column `customer` table:

{{{
#!python
itemdef = define.itemdef(
    __config    = dict(
        name        = 'customer',
        label       = 'Customers',
    ),
    
    id          = string.LabelField(
        label       = 'ID:',
        listing     = True,
        weight      = 1,
    ),
    
    name        = string.StringField(
        label       = 'Name',
        listing     = True,
        weight      = 2,
    ),
)
}}}

This is fully valid Python, but it's presented here as a sort of domain-specific language, much like the FormNode class. Note that the result of the itemdef constructor is bound to a variable; the name of the variable doesn't matter, but the itemdef must be bound to some variable name to be found by the discovery process.

Also like FormNode, Request, and many other components of the modu APIs, it's a dict subclass. For the most part, it consists of a series of names mapped to the field type descriptors called 'definitions' or 'datatypes'.

Since Itemdefs share a common ancestry with FormNode objects, this has lead to a similarity between certain features. For example, like FormNode, additional elements can be added after instantiation:

{{{
#!python
itemdef['city']     = string.StringField(
    label   = 'City',
    listing = True
)

itemdef['state']    = string.SelectField(
    label   = 'State'
    options = {'AK':'Arkansas', 'AL':'Alabama', ... }
)
}}}

One thing that you should notice with as a difference between these two usage variations is that in the first example, we are required to provide 'weight' arguments to indicate the order of the fields. Since Python dictionaries don't preserve key order, and there's no way to override the type of object used to contain `**kwarg`-based arguments, this means a weight value must be provided.

In the second example, no weight argument is necessary, but may be used if desired.

When the administrative interface needs to render a particular itemdef for the user, it calls the itemdef's get_form() method, which generates a standard modu FormNode instance. It is therefore possible to use itemdef-derived forms outside the administrative interface, although at this time that feature has not been particularly well-tested.

=== Initial Configuration ===

The first step in enabling use of the admin tool is to activate it inside your modu site configuration. If you used mkmodu to create your project folder, the admin tool should already be enabled with a couple of basic itemdefs. Otherwise, you can add the admin Resource as follows:

{{{
#!python
    import myproject.itemdefs
    
	application.activate('/admin', resource.AdminResource,
	    default_path   = 'admin/listing/page', 
	    itemdef_module = myproject.itemdefs,
	)
}}}

The two keyword arguments specified here can be omitted, but are usually not. The first argument, 'default_path' specifies the modu path that should be automatically redirected to on login. If omitted, the user is simply taken to the first available itemdef; however, if the user doesn't have access to that itemdef, an error will occur.

For more complex authentication scenarios, 'default_path' can be a function that takes the request object and returns some path, like this:

{{{
#!python
    def admin_path_callback(req):
        if(req.user.has_role('administrator')):
            return 'admin/listing/user'
        else:
            return 'admin/listing/page'
}}}

The `itemdef_module` keyword argument allows you to specify a python module/package to be scanned for itemdefs. This value can also be a sequence, allowing use of itemdefs from multiple packages.

=== Itemdef Development ===

Configuration options and variables can vary between different field types, but there are a number of standard configuration options that are applicable to every itemdef. They are specified by being included in the `__config` argument to the itemdef constructor, or by the `config` instance variable.

 '''name'''::
 '''Required'''::
    A unique identifier for this itemdef, usually the table name

 '''table'''::
 Default: `<table-name>`::
    If the identifier is not the table name, it must be set here

 '''label'''::
 Default: `<itemdef-name>`::
    The display name of this itemdef

 '''category'''::
 Default: `'other'`::
    The itemdef category

 '''weight'''::
 Default: `0`::
    The position of this itemdef in relation to others in its category

 '''acl'''::
 Default: `''`::
    The required user permission to use this itemdef. Can be provided as a single string permission required, or a list of required permissions.

 '''prewrite_callback'''::
 Default: `None`::
    Called during validation. If it returns False, validation will fail.

{{{
#!python
def prewrite_callback(req, form, storable):
    return True
}}}

 '''postwrite_callback'''::
 Default: `None`::
    Called during submit, after writing the storable. If it returns False, an error will be raised (but the main record will have already been written).

{{{
#!python
def postwrite_callback(req, form, storable):
    return True
}}}

 '''predelete_callback'''::
 Default: `None`::
    Called during validation. If it returns False, deletion will fail.

{{{
#!python
def predelete_callback(req, form, storable):
    return True
}}}

 '''postdelete_callback'''::
 Default: `None`::
    Called during submit, after deleting the storable.

{{{
#!python
def postdelete_callback(req, form, storable):
    pass
}}}

 '''theme'''::
 Default: `theme.Theme`::
    Called during submit, after deleting the storable.

 '''factory'''::
 Default: `storable.DefaultFactory`::
    Factory to use to build Storable objects.

 '''model_class'''::
 Default: `storable.Storable`::
    Model class to use with DefaultFactory.

 '''list_template'''::
 Default: `'admin-listing.tmpl.html'`::
    Overrides the default list template.

 '''detail_template'''::
 Default: `'admin-detail.tmpl.html'`::
    Overrides the default detail template.

 '''template_variable_callback'''::
 Default: `None`::
    A dict of name-value pairs returned from this function will be added to the template.

{{{
#!python
def template_variable_callback(req, form, storable):
    return {'myvariable':'myvalue'}
}}}

 '''title_column'''::
 Default: `'title'`::
    The name of a result column that should be used as the title.

 '''no_create'''::
 Default: `False`::
    Disable creation for this itemdef.

 '''per_page'''::
 Default: `25`::
    The number of results to show per listing page.

 '''listing_title'''::
 Default: `'Listing <Name> Records'`::
    The title to be displayed on listing pages.

 '''export_query_builder'''::
 Default: `None`::
    When using the export feature, this function will be called to generate the query.

{{{
#!python
def export_query_builder(req, itemdef, attribs):
	return sql.build_select(itemdef.name, attribs)
}}}

 '''export_type'''::
 Default: `'csv'`::
    Export type (may be 'csv' or 'tsv').

 '''export_le'''::
 Default: `'\n'`::
    Line endings to use in exported CSV.

 '''export_formatter'''::
 Default: `None`::
    When using the export feature, this function will be called to generate the query.

{{{
#!python
def export_formatter(req, itemdef, item):
	# Use m.u.OrderedDict to enforce column order
	return dict()
}}}

 '''export_callback'''::
 Default: `None`::
    When using the export feature, this function will be called after the result rows have been fetched.

{{{
#!python
def export_callback(req):
	pass
}}}

 '''resource'''::
 Default: `None`::
    If `resource` is defined, this resource will provide the content for this itemdef while most other itemdef configuration variables (and any defined fields) will be ignored.

=== Itemdef List View ===

When the use first visits an itemdef through the administrative interface, they are presented with the listing view, which provides a paginated list of rows available in the related database table.

Only fields that have their `listing` attribute set to `True` will be displayed here. Also, at this time there is no support for inline editing in list view; when a field is presented in the list, it should be rendered as read-only.

Additionally, to actually view a record in detail view, at least one of the listed fields must also have its `link` attribute set to `True`. There are exceptions to this when dealing with custom field controls, but in most cases it is a required step to make a field link to its record's detail view.

=== Itemdef Detail View ===

The detail view is essentially the primary view of a given itemdef. Although various fields may be omitted due to the available permissions of a given user, generally the itemdef detail view provides a controlled way to allow an authorized user to make changes to a single database record (as well as any related records).

'''NOTE:''' There seems to be a potential issue with the current implementation of detail views that can cause problems with certain users. It's a result of a not uncommon misunderstanding about what a web page is.

When you open a detail view of a record, that page is a snapshot of the state of the database at the time that the form was rendered. Consequently, if another user (or the same user in a different window) changes the content of the record being viewed, and then the viewed record is changed, the initial changes will be lost.

=== Searching for Records ===

Most of the standard field types included with modu support some kind of searching capability, which is enabled by setting an itemdef field's `search` attribute to `True`. For example, here's a `name` field from an arbitrary itemdef:

{{{
#!python
itemdef['name'] = string.StringField(
    label           = 'Name',
    search          = True,
    search_style    = 'substring'
)
}}}

This example will add a search field in the listing view of this itemdef. When the search is submitted, the list view will display only records with `name` fields that contain the exact substring provided. Note that in this case, the `search_style` parameter may be omitted, since `'substring'` is already the default.

Other search styles include `'exact'` which returns only records with exact matches, and `'fulltext'`, which will use `MATCH(column) AGAINST ('pattern')` to enable leveraging MySQL's fulltext support.

=== Validation/Submission Overrides ===

As previously mentioned, when the administrative interface needs to render a particular itemdef for the user, it calls the itemdef's get_form() method, which generates a standard modu FormNode instance. This FormNode has already had its validation and submission functions defined to adhere to the configuration provided in the given itemdef.

There's a number of steps executed during the validation and submission process. First, the validation steps:

 1. Was the cancel button was clicked?
  * return to listing if `True`
 2. Was the delete button clicked?
  1. Is deletion enabled?
   * return to listing if `no_delete` is `True`
  2. Call `predelete_callback`, if available
   * abort deletion if it returns `False`
  3. Is `delete_related_storables` on for this itemdef?
   * delete related storables if `True`
  4. Delete storable
  5. Call `postdelete_callback`
 3. Was the button clicked a custom submit button?
  1. Is there a `validator` callable set on the button?
   * continue only if it returns `True`
  2. Otherwise report error and fail validation
 4. Iterate through fields
  1. Does field have `validator` callable?
   * continue only if it returns `True`
  2. Call standard FormNode validation function
   * continue only if it returns `True`