OxyScripts.com
Menu spacer Home Tutorials Articles Code Forums irc.freenode.net #oxyscripts
Main (PHP)
Home Forums PHP News PHP Tutorials Articles PHP Code Snippets Contact Us Sysadmin Resources Books Template Shop
3rd Party Streams
SlashDot PHPDeveloper.org PHP.Net
Resources
PHP Manual MySQL Manual Smarty Manual PEAR Manual PHP-GTK Manual Symfony Manual
Code Snippets
Authentication Database Graphics HTTP Miscellaneous Time/Date
Affiliates
Scripts TutorialMan TutorialGuide CodingForums.com PHP Scripts Cheap Web Hosting Affordable Web Hosting Dreamweaver Templates

Search This Site :     PHP Function Reference :
 

The admin generator

Overview

Backend administrations are often built according to the structure of the data used in the frontend application. Such backends can be entirely generated by symfony, provided your object model is well defined. The mechanism that does that is called the admin generator. It automates many repetitive tasks using a system of custom configuration. It also gives you total control, allowing you to customize or extend all the generated components and the look and feel of the application. And when the admin generator can't fulfill your requirements, it provides the tools to plug your own code into the generated administration.

Note: A screencast showing an administration being built is available for download (21min, QT7, 17 Mb).

Note: A cheat sheet with the admin generator parameters explained briefly in a single page is available for download (pdf, 28 Kb).

Note: If you need a basic CRUD interface to initiate the templates and actions of a module for your frontend application, you will probably prefer making a scaffolding than a generated admin.

Introduction

For this chapter, the example used will be a blog application with two Article and Comment classes, based the following table structure:

blog_article blog_comment
id id
title article_id
content author
date
content

The application in which the administration will be built is called backend.

Initiating an admin module

With symfony, you build an administration module by module. A module is generated based on a Propel object, using the command line interface with the propel-init-admin task, with <APPLICATION_NAME>, <MODULE_NAME> and <CLASS_NAME> as parameters:

$ symfony propel-init-admin backend article Article

This single command is enough to create an article module with list, edit, create and delete actions, based on the Article Propel class, and accessible by:

http://www.example.com/backend.php/article

The look and feel of a generated module is sophisticated enough to make it usable out of the box for a commercial application.

Modules generated by the admin generator benefit from the usual module mechanisms (decorator, validation, routing, custom configuration, autoloading, etc.). You are free to link the modules as you want, or to add a module of your own.

The generated code

If you look at the generated code in the apps/backend/modules/article/ directory, you will find an empty action class and no templates. This is because the module inherits from classes and files located in the framework file structure. You can find the code which is actually used in the cache/backend/ENV/modules/article folder, once you've used it:

actions/actions.class.php
  create -> edit
  delete
  edit
  index -> list
  list
  save -> edit

templates/
  _edit_actions.php
  _edit_footer.php
  _edit_header.php
  _filters.php
  _list_actions.php
  _list_footer.php
  _list_header.php
  _list_td_actions.php
  _list_td_stacked.php
  _list_td_tabular.php
  _list_th_stacked.php
  _list_th_tabular.php
  editSuccess.php
  listSuccess.php

This shows that a generated admin module is composed mainly of two views, edit and list. If you have a look at the code, you will find it to be very modular, readable and extensible.

Inheriting actions and templates allows you to override them completely. For instance, you can use your filters by adding your own _filters.php in the modules/articles/templates/ directory.

The generator.yml configuration file

Most of the customization of a generated administration is done through a YAML configuration file called generator.yml. To see the default configuration of an administration module just created, open the backend/modules/article/config/generator.yml file:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Article
    theme:            default

This configuration is enough to generate the basic administration. The customization is added under the param: key (which means that all lines added in the generator.yml mus at least start with four blank spaces). Here is a typical customized generator.yml for a post module (extracted from the screencast source):

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Post
    theme:            default

    fields:
      author_id:      { name: Post author }

    list:
      title:          symfony blog posts
      display:        [title, author_id, category_id]
      fields:
        published_on: { params: date_format='dd/MM/yy' }
      layout:         stacked
      params:         %%is_published%%<strong>%%=title%%</strong><br /><em>by %%author%% in %%category%% (%%published_on%%)</em><p>%%content_summary%%</p>
      filters:        [title, category_id, author_id, is_published]
      max_per_page:   2

    edit:
      title:          Editing post "%%title%%"
      display:
        "Post":       [title, category_id, content]
        "Workflow":   [author_id, is_published, created_on]
      fields:
        category_id:  { params: disabled=true }
        is_published: { type: plain}
        created_on:   { type: plain, params: date_format='dd/MM/yy' }
        author_id:    { params: size=5 include_custom=>> Choose a blog author << }
        published_on: { credentials: [[admin, superdamin]] }
        content:      { params: rich=true tinymce_options=height:150 }

The following sections explain in detail the parameters that can be used in this configuration file.

Fields

Fields settings

The configuration file can define how columns appear in the pages. For instance, to define a custom label for the title and content columns in the article module, edit the generator.yml:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Article
    theme:            default

    fields:
      title:          { name: Article Title }
      content:        { name: Body }

In addition to this default definition for all the views, you can override the fields settings for a given view (list and edit):

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Article
    theme:            default

    fields:
      title:          { name: Article Title }
      content:        { name: Body }

    list:
      fields:
        title:        { name: Title }

    edit:
      fields:
        content:      { name: Body of the article }

This is a general principle: The settings that are set for the whole module under the param key can be overridden for a given view.

Adding fields to the display

The fields that you define in the fields section can be displayed, hidden, ordered and grouped in various ways for each view. The display: key is used for that purpose. For instance, to arrange the fields of the comment module, edit the modules/comment/config/generator.yml:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Comment
    theme:            default

    fields:
      id:             { name: Id }
      article_id:     { name: Article }
      author:         { name: Author }
      date:           { name: Published on }
      content:        { name: Body }

    list:
      display:        [id, article_id, date]

    edit:
      display:
        "NONE":       [id, article_id]
        "Editable":   [date, author, content]

If you don't supply any group name (like in the list view above), put the fields that you want to display in an ordered array. If you want to group fields, use an associative array with the group name as a key, or "NONE" for a group with no name.

Custom fields

As a matter of fact, the fields parameters don't need to be actual columns in the tables. If you define a custom getter and setter, it can be used as a field as well. For instance, if you extend the Article.class.php model by a ->getNbComments() method:

public function getNbComments()
{
  return count($this->getComments());
}

You can use nb_comments as a field in the admin (notice that the getter uses a camelCase version of the field name):

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Article
    theme:            default

    fields:
      title:          { name: Article Title }
      content:        { name: Body }
      nb_comments:    { name: Number of comments }

    list:
      display:        [id, title, nb_comments]

    ...

Custom fields can even return HTML code to display more than raw data. For instance, if you extend the Comment class in the model with a ->getArticleLink() method:

public function getArticleLink()
{
  return link_to($this->getArticle()->getTitle(), 'article/edit?id='.$this->getArticleId());
}

You can use it in the comment/list view by editing the related generator.yml:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Comment
    theme:            default

    fields:
      id:             { name: Id }
      article_link:   { name: Article }
      author:         { name: Author }
      date:           { name: Published on }
      content:        { name: Body }

    list:
      display:        [id, article_link, date]
      ...

Partial fields

Beware that the code located in the model must be independent from the presentation. The example of the getArticleLink() method presented above doesn't respect this principle of layer separation. To achieve the same goal in a conceptually correct way, you'd better put the code that outputs HTML for a custom field in a partial. Fortunately the admin generator allows you to do it if you declare a field with a name starting by _ (and, in that case, you don't need to add custom methods to the model):

    ...
    list:
      display:        [id, _article_link, date]

For this to work, just add the following _article_link.php partial in the modules/comment/templates/ directory:

<?php echo link_to($comment->getArticle()->getTitle(), 'article/edit?id='.$comment->getArticleId()) ?>

Notice that the partial template of a partial field has access to the current object through a variable named by the class ($comment in this example). For instance, for a module built for a class called UserGroup, the partial will have access to the current object through the $user_group variable.

Note: If you need to customize the parameters of a partial field (_article_link in this example), do the same as for a normal field, under the field: key. Just don't include the first underscore (_):

    fields:
      article_link:   { name: Article }

If your partial gets crowded with more and more logic, you'll probably want to replace it by a component. Change the _ prefix by a ~ and you can define a component column:

    ...
    list:
      display:        [id, ~article_link, date]

In the generated template, this will result by a call to the articleLink component of the current module.

Custom and partial fields can be used in the list view and in the edit view.

Customizing the views

Title

In addition to a custom set of fields, the list and edit pages can have a custom page title. In the string values of the generator.yml, the value of a field can be accessed via the name of the field surrounded by %%. For instance, if you want to customize the article views:

    list:
      title:          List of Articles
      display:        [title, content, nb_comments]

    edit:
      title:          Body of article %%title%%
      display:        [content]

Tooltips

In the list and edit views, you can add tooltips to help describe the fields that are displayed. For instance, to add a tooltip to the article_id field of the edit view of the comment module, add:

    edit:
      fields:
        ...
        article_id:   { help: The current comment relates to this article }

In the list view, the tooltip will be displayed in the column header, and in the edit view, the tooltip will be displayed close to each field.

Date format

Dates can be displayed using a custom format as soon as you use the date_format param:

    list:
      fileds:
        date:         { name: Published, params: date_format='dd/MM' }

It takes the same format parameter as the format_date() helper described in the internationalization helpers chapter.

list view specific customization

Layout

The default list layout is the tabular layout, but you can also use the stacked layout. A field name preceded by = will contain a hyperlink to the detail of the related record. For instance, if you want to customize the article/list views:

    list:
      title:          List of Articles
      layout:         tabular
      display:        [=title, content, nb_comments]

And the comment/list view:

    list:
      title:          List of Comments
      layout:         stacked
      params:         %%=content%% (sent by %%author%% on %%date%% about %%article_link%%)
      display:        [date, author, content]

Notice that a tabular layout expects a display, but a stacked layout uses the params key for the HTML code generated for each record. However, the display param is still used in a stacked layout which columns are available for the interactive sorting.

Filters

In a list view, you can add a filter interaction, to help the user find a given set of records. For instance, to add filters to the comment/list view:

    list:
      filters:        [author, article_id]

The resulting filter will allow text-based search on an author name (where the * character can be used as a joker), and the selection of the comments related to a given article by a choice in a selection list. As for regular object_select_tag(), the options displayed in a select are the ones returned by the ->toString() method of the related class (or the primary key if such a method doesn't exist).

Just like you use partial fields in lists, you can also use partial filters to create a filter that symfony doesn't handle on its own. For instance, imagine a state field that may only contain 2 values (open and closed), but for some reason you store those values directly in the field instead of using a table relation. A simple filter on this field (of string type) would be a text-based search, but what you want is probably a select with a list of values. That's quite easy to achieve with a partial filter:

    list:
      filters:        [date, _state]

Then, in templates/_state.php:

<?php echo select_tag('filters[state]', options_for_select(array(
  '' => '',
  'open' => 'open',
  'closed' => 'closed',
), isset($filters['state']) ? $filters['state'] : '')) ?>

Notice that the partial has access to a $filters variable, which is very useful to get the current value of the filter.

Sort

In a list view, the column names are hyperlinks that can be used to reorder the list. These names are displayed both in the tabular and stacked layouts. Only the fields that correspond to an actual column are clickable - not the ones for custom or partial columns.

Clicking on these links reloads the page with a sort parameter. You can reuse the syntax to point to a list directly sorted according to a column:

<?php echo link_to('Comment list by date', 'comment/list?sort=date&type=desc' ) ?>

You can also define a default sort field for the list view directly in the generator.yml:

    list:
      sort:   date

Or, if you want to specify the sort order:

    list:
      sort:   [date, desc]

Pagination

The generated administration deals with large tables like a charm. The list view uses pagination by default, and you can customize the number of records to be displayed in each page with the max_per_page parameter:

    list:
      max_per_page:   5
      title:          List of Comments
      layout:         stacked
      params:         %%=content%% (sent by %%author%% on %%date%% about %%article_link%%)

Join

The default method used by the admin generator to get the list is a doSelect(). But, if you use related objects in the list, the number database queries executed will rapidly increase. For instance, if you want to display the name of the author in a list of posts, an additional query will be made to the database for each line in the list to retrieve the related Author object.

You may want to force the pager to use a doSelectJoinXXX() method to optimize the number of queries. This can be specified with the peer_method parameter:

    list:
      peer_method:   doSelectJoinAuthor

edit view specific customization

Input type

In an edit view, the user can modify the value of each field. Symfony determines the type of input to be used according to the data type of the column. For instance, fields finishing with _id will be displayed as select inputs. However, you may want to force a certain type of input for a given field, or the options of the object_tag generated. This kind of parameter goes into the fields definition:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Comment
    theme:            default

    fields:
      id:             { name: Id }
      article_id:     { name: Article }
      author:         { name: Author }
      date:           { name: Published on }
      content:        { name: Body }

    ...

    edit:
      fields:
        id:           { type: plain }                              ## Drop the input, just display plain text
        author:       { params: disabled=true }                    ## The input is not editable
        content:      { type: textarea_tag, params: rich=true css=user.css tinymce_options=width:330 }  ## The input is a textarea (object_textarea_tag)
        article_id:   { params: include_custom=Choose an article } ## The input is a select (object_select_tag)

      ...

The params parameters are passed as options to the generated object_tag. For instance, the params definition for the article_id above will produce:

<?php echo object_select_tag($comment, 'getArticleId', 'related_class=Article', 'include_custom=Choose an article') ?>

This means that all the options available in the form helpers can be customized in an edit view.

Partial fields handling

Partial fields can be used in edit views just like in list views. The difference is that you have to plug the control(s) of the partial with the fields of the objects, by hand, in the action. For instance, an administration module for a User model object where the available fields are id, nickname and password, will probably not display the password field in clear, for security reasons. Instead, such modules often offer an empty password input that the user has to fill to change the value:

    edit:
      display:        [id, nickname, _newpassword]
      fields:
        newpassword:  { name: Password, help: Enter a password to change it, leave the field blank to keep the current one }

The templates/newpassword.php partial contain something like:

<?php echo input_tag('newpassword', '') ?>

Notice that this partial uses a simple form helper, not an object form helper, since it is not intended to get the value from the current object.

Now, in order to use the value from this control to update the object in the action, you need to extend the updateUserFromRequest() method. To do that, create the following function in the module actions.class.php with the custom behaviour for the input of the partial field:

class autoUserActions extends sfActions
{
  protected function updateUserFromRequest()
  {
    $password = $this->getRequestParameter('newpassword');
 
    if ($password != '')
    {
      $this->user->setPassword($password);
    }
 
    parent::updateUserFromRequest();
  }
}

Note: In the real world, a user/edit view usually contains two password fields, the second having to match the first one to avoid typing mistakes. In practice, this is done via a validator. The admin generated modules benefit from this mechanism just like regular modules.

Table relationships

One to many

The 1-n table relationships are taken care of by the admin generator. In the example mentioned above, the weblog_comment table is related to the weblog_article table via the article_id field. If you initiate the module of the Comment class with the admin generator:

$ symfony propel-init-admin myapp comment Comment

The comment/edit action will automatically display the article_id as a select list showing the ids of the available records of the weblog_article table. In addition, if you define a __toString() method in the Article object, the string returned by this method is used instead of the ids in the select list.

If you need to display the list of comments related to an article in the article module (n-1 relationship), you will need to customize the module a little by the way of a partial field.

Many to many

The n-n table relationships are also taken care of. The implementation of such relationships is made through an intermediate table. For instance, if there is a n-n relation between a blog_article and a blog_author table (an article can be written by more than one author and, obviously, an author can write more than one article), then your database will always end up with a table called blog_article_author or similar.

blog_article blog_article_author blog_author
id article_id id
title author_id name
... ...

The model will then have a class called ArticleAuthor and this is the only thing that the admin generator needs - but you have to pass it as a parameter of the field, called trough_class.

For instance, in a generated module based on the Article class, you can add a field to create new n-n associations with the Author class.

    edit:
      fields:
        article_author: { type: admin_double_list, params: through_class=ArticleAuthor }

Such a field handles links between existing objects, so a regular select list is not enough. You must use a special type of input for that. Symfony offers three widgets to help relate members of two lists. You have the choice between an admin_double_list, an admin_select_list and an admin_check_list.

Interactions

Admin modules are made to interact with the data. The basic interactions that can be performed are the usual CRUD, but you can also add your own interactions or restrict the possible interactions for a view. For instance, the following interaction definition for the article module gives access to all the CRUD actions:

    list:
      title:          List of Articles
      object_actions:
        _edit:        -
        _delete:      -          
      actions:
        _create:      -

    edit:
      title:          Body of article %%title%%
      actions:        
        _list:        -
        _save:        -
        _delete:      -

In a list view, there are two action settings: The list of actions available for every object, and the list of actions available for the whole page. In an edit view, as there is only one record edited at a time, there is only one set of actions to define.

The _XXX lines tell symfony to use the default icon and action for these interactions. But you can also add a custom interaction:

    list:
      title:          List of Articles
      object_actions:
        _edit:        -
        _delete:      -
        addcomment:   { name: Add a comment, action: addComment, icon: backend/addcomment.png }

You also have to define the addForArticle in actions.class.php:

public function executeAddComment()
{
  $comment = new Comment();
 
  $comment->setArticleId($this->getRequestParameter('id'));
 
  $comment->save();
 
  $this->redirect('comment/edit?id='.$comment->getId());
}

Notice that symfony is smart enough to pass the primary key of the object for which the action was called as a request parameter.

One last word about actions: If you want to suppress completely the actions for one category, use an empty list:

    list:
      title:          List of Articles
      actions:        {}

Form validation

If you take a look at the generated editSuccess.php template in your project cache/ directory, you will see that the form fields use a special naming convention. In a generated edit view, the input names are made with the name of the module concatenated to the name of the field between angle brackets.

For instance, if the edit view for the article module has a title field, the template will look like:

<?php echo object_input_tag($article, 'getTitle', array('control_name' => 'post[title]')) ?>
// will generate in html
<input type="text" name="article[title]" id="article[title]" value="My Title" />

This has plenty of advantages during the form handling process. However, it makes the form validation configuration a bit trickier, since this format messes up with the YAML syntax. For instance, you can't write in a validation file for an admin edit view:

methods:
  names: [article[title], article[body]]

You will have to enclose the field names between double quotes " " and change square brackets [ ] by curly brackets { }. Additionally, to avoid YAML interpretation problems, you should use the explicit YAML syntax for arrays. It makes the validation file look like:

methods:
  post: 
    - "article{title}"
    - "article{body}"

When declaring the validators for a field, you don't need the double quotes but you must keep the curly brackets:

names:
  article{title}:
    required:     Yes
    required_msg: You must provide a title

Lastly, when using a field name as a parameter for a validator, you should use the name as it appears in the generated HTML code, i.e. with the square brackets, but between quotes:

passwordValidator:
  class:          sfCompareValidator
  param:
    check:        "user[newpassword]"
    compare_error: The password confirmation does not match the password. Please try again.

Presentation

Custom stylesheet

You can also define an alternative CSS to be used for an admin module instead of a default one:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Comment
    theme:            default
    css:              admin/mystylesheet

... or override the styles per view, using the usual mechanisms provided by the module view.yml configuration. Since the generated HTML is structured content, you can do pretty much anything you like with the presentation.

Custom header and footer

The list and edit views can include a custom header and footer partial. There is no such partial by default in the templates/ directory of an admin module, but you just need to add one with one of the following names to have it included automatically:

_list_header.php
_list_footer.php
_edit_header.php
_edit_footer.php

For instance, if you want to add a custom header to the article/edit view, create a file called _edit_header.php in the modules/articles/template/ directory with the following content:

<?php if($article->getNbComments()>0): ?>
  <h2>This article has <?php echo $article->getNbComments() ?> comments.</h2>
<?php endif; ?>

Notice that an _edit partial always has access to the current object through a variable having the same name as the module, and that a _list partial always has access to the current list of objects through the plural form variable ($articles for this module).

Custom template parts

There are other partials inherited from the framework that can be overridden in the module templates/ folder to match your custom requirements:

_edit_actions.php
_filters.php
_list_actions.php
_list_td_actions.php
_list_td_stacked.php
_list_td_tabular.php
_list_th_stacked.php
_list_th_tabular.php

Get the default version from the symfony/data/generator/sfPropelAdmin/default/template/templates/ directory.

Calling the admin actions with custom parameters

The actions created for an administration can receive custom parameters using the query_string argument in a link_to() helper. For example, to extend the previous _edit_header with a link to the comments for the article, write:

<?php if($article->getNbComments()>0): ?>
  <?php echo link_to('View the '.$article->getNbComments().' comments to this article.', 'comment/list', array('query_string' => 'filter=filter&filters%5Barticle_id%5D='.$article->getId())) ?>
<?php endif; ?>

The query string presented above is an encoded version of the more legible:

'filter=filter                            # filters are to be reseted with the following params
 filters[article_id]='.$article->getId(); # filter the comments to display only the ones related to $article

Using the query_string argument, you can specify a sorting order and/or a filter to display a custom list view.

This can also be useful for custom interactions.

Credentials

For a given admin module, the elements displayed and the actions available can vary according to the credentials of the logged user.

The generator can take a credentials parameter into account to hide an element to users who don't have the proper credential if you use it in the fields section:

## The `id` column is displayed only for user with the `admin` credential
    list:
      title:          List of Articles
      layout:         tabular
      display:        [id, =title, content, nb_comments]
      fields:
        id:           { credentials: [admin] }

This works for the list view and the edit view.

The generator can also hide interactions according to credentials:

## The `addcomment` interaction is restricted to the users with the `admin` credential
    list:
      title:          List of Articles
      object_actions:
        _edit:        -
        _delete:      -
        addcomment:   { credentials: [admin], name: Add a comment, action: addComment, icon: backend/addcomment.png }

The credentials parameter accepts the usual credentials syntax, which allows you to combine credentials with AND and OR:

credentials: [ admin, superuser ]          ## admin AND superuser
credentials: [[ admin, superuser ]]        ## admin OR superuser
credentials: [[ admin, superuser ], owner] ## (admin OR superuser) AND owner

If you want to learn more about credentials, please refer to the security chapter of the symfony book.

Customize the theme

If you customize several modules in the same way, you should probably create a theme that could be reused across modules. The theme defined at the beginning of the generator.yml can be changed to use an alternative set of templates and stylesheets. With the default theme, symfony uses the files defined in $sf_symfony_data_dir/generator/sfPropelAdmin/default, and you can create a new theme in the framework to override any of the actions or templates.

The generator templates are cut into small parts that can be overridden independently, and the actions can also be changed one by one.

To create your own theme, add a new directory in the $sf_symfony_data_dir/generator/sfPropelAdmin/ folder and fill it with your custom version of the following elements, using the same structure as the default/ theme:

fragments:
_edit_actions.php
_edit_footer.php
_edit_header.php
_filters.php
_list_actions.php
_list_footer.php
_list_header.php
_list_td_actions.php
_list_td_stacked.php
_list_td_tabular.php
_list_th_stacked.php
_list_th_tabular.php

actions:
processFilters()     // process the request filters
addFiltersCriteria() // adds a filter to the Criteria object
processSort()       
addSortCriteria()

Translation

All the texts that are in the generated templates are already internationalized (i.e. enclosed in a call to the __('') function). This means that you can easily translate a generated admin by adding the translations of the texts in a XLIFF file, in your apps/myapp/i18n/ directory, as explained in the i18n chapter.

 
   Print this page

Top Sponsor
Symantec\'s Norton SystemWorks 2006
Sponsors
CA
Sponsors
AdWords Dominator 125*125
Advertisting

Affiliates
VertexTemplates PHPFreaks CodeWalkers StarGeek DevScripts CGI & PHP Scripts PHP CMS

Shopping Rebates   Sell It 4 You   Flash Page Counters   Get Insured
GPS Tracking Service   Charity Donate Info   Web Site Hosting   VOIP Service

Privacy Policy | Links | Site Map | Advertising

All content on OxyScripts.com is (©)2002-2007

 
Powered by Adrastea - Version 1.0.0. Copyright © Rune Solutions, 2004-2005