Xadmin Django thème bootstrap twitter

On ne va pas se mentir, de ce que nous avons vu de la partie admin ce n'est pas l'extase visuel. Il existe des projets -sur github notamment- qui proposent d'améliorer le design de votre projet Django. Le module que j'utilise le plus souvent est xadmin puisqu'en plus d'être sympatique visuellement (basé sur bootstrap twitter) il propose quelques plugins très utiles, comme les exports en CSV, XML et JSON mais également des filtres très bien pensés.

Démo de xadmin

Arrêtons les éloges, vous pouvez visiter le site de démonstration pour vous faire votre propre idée: Demo xadmin .
Les identifiants sont admin / admin.

Installer xadmin

Un petit pip pour installer xadmin:

pip install django-xadmin

Le projet est sur Github: xAdmin

Implémenter xadmin

Il vous faudra indiquer la présence de xadmin et crispy_form dans le partie settings:

eboutique/settigns.py

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'backoffice',
    'crispy_forms',
    'xadmin',
)

Puis configurez le fichier urls.py

eboutique/urls.py

from django.conf.urls import patterns, include, url
import xadmin

xadmin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'eboutique.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),
    #url(r'^admin/', include(admin.site.urls)),

    url(r'^xadmin/', include(xadmin.site.urls)),
)

Nous avons défini que le module xadmin est accessible via l'uri /xadmin/

Premières captures

http://localhost:8000/xadmin

Vous pouvez vos connecter à l'interface xadmin en indiquant les identifiants que vous avez renseigné lors de votre syncb.

Responsive design

Format grand écran:

Format tablette:

Format smartphone:

Afficher ses modèles

Dans nos exemples nous utiliserons les modèles suivants:

backoffice/models.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.db import models

class Product(models.Model):
    """
    Produit : prix, code, etc.
    """
    
    class Meta:
        verbose_name = "Produit"
        
    name       = models.CharField(max_length=100)
    code       = models.CharField(max_length=10, null=True, blank=True, unique=True)
    price_ht   = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire HT")
    price_ttc  = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire TTC")
    
    def __unicode__(self):
        return u"{0} [{1}]".format(self.name, self.code)

class ProductItem(models.Model):
    """
    Déclinaison de produit déterminée par des attributs comme la couleur, etc.
    """
    
    class Meta:
        verbose_name = "Déclinaison Produit"
        
    product     = models.ForeignKey('Product')
    code        = models.CharField(max_length=10, null=True, blank=True, unique=True)
    code_ean13  = models.CharField(max_length=13)
    attributes  = models.ManyToManyField("ProductAttributeValue", related_name="product_item", null=True, blank=True)
       
    def __unicode__(self):
        return u"{0} [{1}]".format(self.product.name, self.code)
    
class ProductAttribute(models.Model):
    """
    Attributs produit
    """
    
    class Meta:
        verbose_name = "Attribut"
        
    name =  models.CharField(max_length=100)
    
    def __unicode__(self):
        return self.name
    
class ProductAttributeValue(models.Model):
    """
    Valeurs des attributs
    """
    
    class Meta:
        verbose_name = "Valeur attribut"
        ordering = ['position']
        
    value              = models.CharField(max_length=100)
    product_attribute  = models.ForeignKey('ProductAttribute', verbose_name="Unité")
    position           = models.PositiveSmallIntegerField("Position", null=True, blank=True)
     
    def __unicode__(self):
        return u"{0} [{1}]".format(self.value, self.product_attribute)

Comme pour la partie admin de Django, il est nécessaire d'indiquer à xadmin quel modèle nous voulons afficher dans l'interface. Créons un fichier adminx.py dans notre application:

backoffice/adminx.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-

import xadmin
from models import Product

xadmin.site.register(Product)

Vous pouvez maintenant visualiser votre vue Product:

http://localhost:8000/xadmin

Ajoutons un produit:

http://localhost:8000/xadmin/backoffice/product/add/

Ne remplissons pas les champs obligatoires pour voir la réaction de Django:

http://localhost:8000/xadmin/backoffice/product/add/

Validons notre produit correctement

http://localhost:8000/xadmin/backoffice/product/add/

Exporter les données

xadmin vous apporte des options en plus comme la possibilité d'exporter votre liste de données en CSV, XML ou JSON:

Inlines

Comme nous l'avons vu dans le chapitre admin, nous pouvons mettre à la suite de notre formulaire principal un formulaire pour les clés étrangères à l'aide du paramètre inlines:

backoffice/adminx.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-

import xadmin
from models import *

class ProductItemAdminInline(object):
    model = ProductItem
    extra = 1
    
class ProductAdmin(object):
    model = Product
    inlines = [ProductItemAdminInline]

xadmin.site.register(Product, ProductAdmin)

Résultat de notre fiche produit:

Par défaut les items se succèdent les uns à la suite des autres, ce qui en terme d'ergonomie peut dans certains cas être génant, par exemple si les données inlines sont nombreuses. Il existe des options pour changer l'affichage:

class ProductItemAdminInline(object):
    model = ProductItem
    extra = 3
    style = 'tab'

class ProductItemAdminInline(object):
    model = ProductItem
    extra = 3
    style = 'table'

class ProductItemAdminInline(object):
    model = ProductItem
    extra = 3
    style = 'accordion'

Créer une page personnalisée

Un backoffice n'est pas composé que de pages d'édition ou de listing de modèles, vous pouvez par exemple créer une page statistiques:

backoffice/adminx.py

from xadmin import views

class StatsView(views.CommAdminView):

    def get(self, request, *args, **kwargs):
        return self.template_response('xadmin/stats.html', self.get_context())

xadmin.site.register_view(r'stats/$', StatsView, name='stats')

CSS

Amusez-vous à modifier le CSS pour personnaliser votre interface

textarea:focus,input:focus{outline:none!important}
:focus{outline:none!important}
:active{outline:none!important}
.form-control:focus{border-color:#ededed;-webkit-box-shadow:none;box-shadow:none}
.form-horizontal .control-label{margin-top:8px}
.controls{padding:1px!important;margin:0}
.form-horizontal .control-label{padding-top:0!important}
.form-horizontal .controls{border-left:none}
.form-control{display:block;width:100%;padding:2px;font-size:1em;line-height:1.428571429;
color:#555;vertical-align:middle;background-color:#fff;background-image:none;
border:1px solid #ededed;border-radius:0;-webkit-box-shadow:none;box-shadow:none;
-webkit-transition:border-color ease-in-out 0.15s,box-shadow ease-in-out .15s;
transition:border-color ease-in-out 0.15s,box-shadow ease-in-out .15s;margin-top:1px}
ul.nav-sitemenu{padding:0}
#left-side{padding:1px;margin:0}
#left-side a{background-color:#fff;color:#333}
#left-side .active a{background-color:#3276b1;color:#333;font-weight:700;color:#fff}
body{background-color:#eee}
#content-block{padding:0 20px!important}
.form-group{background-color:none;border-bottom:0}
.panel-body{margin:10px 1px}
input[type="file"]{max-width:300px;border:1px solid #efefef}
input[type="number"]{max-width:100px;border:1px solid #efefef;text-align:center}
input[type="email"]{max-width:300px;border:1px solid #efefef}
input[type="text"]{width:100%;border:1px solid #efefef}
input{font-weight:700}
.text-field{max-width:none;height:30px}
.panel .btn{padding:4px}
.panel .input-group-addon{padding:2px 5px}
.input-group-addon,.input-group-btn{width:0}
.textinput{max-width:500px;width:100%}
.text-field{width:70%!important}
.selectize-input{min-height:15px;line-height:13px}
input[data-upper]{text-transform:uppercase}
input[type="text"],input[type="number"],input[type="email"],input[type="file"]
{padding:0;padding-left:5px;line-height:1em;height:30px}
.navbar .navbar-brand{max-width:none}
.select2-container{width:99%}
.select2-choices{max-width:100%}

Les templates de xadmin

Vous trouverez ci-dessous tous les templates du projet xadmin. Pour écraser un template de base, il vous suffit de le recréer dans votre dossier template de votre application avec le même nom.

tree /usr/local/lib/python2.7/dist-packages/xadmin/templates/xadmin/

├── 404.html
├── 500.html
├── auth
│   ├── password_reset
│   │   ├── complete.html
│   │   ├── confirm.html
│   │   ├── done.html
│   │   ├── email.html
│   │   └── form.html
│   └── user
│       ├── add_form.html
│       └── change_password.html
├── base.html
├── base_site.html
├── blocks
│   ├── comm.top.setlang.html
│   ├── comm.top.theme.html
│   ├── comm.top.topnav.html
│   ├── modal_list.left_navbar.quickfilter.html
│   ├── model_form.before_fieldsets.wizard.html
│   ├── model_form.submit_line.wizard.html
│   ├── model_list.nav_form.search_form.html
│   ├── model_list.nav_menu.bookmarks.html
│   ├── model_list.nav_menu.filters.html
│   ├── model_list.results_bottom.actions.html
│   ├── model_list.results_top.charts.html
│   ├── model_list.results_top.date_hierarchy.html
│   ├── model_list.top_toolbar.exports.html
│   ├── model_list.top_toolbar.layouts.html
│   └── model_list.top_toolbar.refresh.html
├── edit_inline
│   ├── accordion.html
│   ├── base.html
│   ├── blank.html
│   ├── one.html
│   ├── stacked.html
│   ├── tab.html
│   └── tabular.html
├── filters
│   ├── char.html
│   ├── checklist.html
│   ├── date.html
│   ├── fk_search.html
│   ├── list.html
│   ├── number.html
│   ├── quickfilter.html
│   └── rel.html
├── forms
│   └── transfer.html
├── grids
│   └── thumbnails.html
├── includes
│   ├── box.html
│   ├── pagination.html
│   ├── sitemenu_accordion.html
│   ├── sitemenu_default.html
│   ├── submit_line.html
│   ├── toggle_back.html
│   └── toggle_menu.html
├── layout
│   ├── fieldset.html
│   ├── field_value.html
│   ├── field_value_td.html
│   ├── input_group.html
│   └── td-field.html
├── views
│   ├── app_index.html
│   ├── batch_change_form.html
│   ├── dashboard.html
│   ├── form.html
│   ├── invalid_setup.html
│   ├── logged_out.html
│   ├── login.html
│   ├── model_dashboard.html
│   ├── model_delete_confirm.html
│   ├── model_delete_selected_confirm.html
│   ├── model_detail.html
│   ├── model_form.html
│   ├── model_history.html
│   ├── model_list.html
│   ├── quick_detail.html
│   ├── quick_form.html
│   ├── recover_form.html
│   ├── recover_list.html
│   ├── revision_diff.html
│   └── revision_form.html
└── widgets
    ├── addform.html
    ├── base.html
    ├── chart.html
    ├── list.html
    └── qbutton.html

Intégrer font awesome

La version font awesome intégrée ne propose pas toutes les icones. Vous pouvez importer le projet font awesome en modifiant le fichier base.html

backoffice/template/xadmin/base.html

<link rel="stylesheet" type="text/css" href="{% static "css/font-awesome.min.css" %}" />

N'oubliez pas d'ajouter le projet font-awesome dans votre dossier static.

Ajouter une ligne dans votre menu

Les menus sont construit dans le template sitemenu_default.html

backoffice/templates/xadmin/includes/sitemenu_default.html

<li class="nav-header">Actions</li>
<li class="{% if request.path == "/xadmin/stats/" %}active{% endif %}">
<a href="{% url 'xadmin:stats' %}"><i class="fa-fw fa fa-line-chart"></i> Statistique</a>
</li>

À noter que request.path nécessite une action de votre part expliquée dans le chapitre context processor.

Résultat:

Associer une icone à une vue

Les icones cercle par défaut sont assez fades, vous pouvez ajouter votre propres icones font awsome en renseignant l'attribut model_icon:

backoffice/adminx.py

class ProductAdmin(object):
    model = Product
    model_icon = "fa fa-barcode"

class ProductAttributeAdmin(object):
    model= ProductAttribute
    model_icon = "fa fa-list-ol"

Résultat:

Manipuler depuis les valeurs POST envoyées

Vous pouvez redéfinir la méthode post pour ajouter des contrôles ou modifier des données lors de la soumission du formulaire.

backoffice/adminx.py

class ProductAdmin(object):
    model = Product 
  
    def post(self, request, *args, **kwargs):
        try:
            self.instance_forms()
            self.setup_forms()
            
            if self.valid_forms():
                self.message_user("Attention ceci et cela bla bla", 'warning')
                return self.get_response()      
        except:
            pass

        return super(TicketAdmin, self).post(request, *args, **kwargs)

Pour info vous pouvez récupérer les données soumis par le formulaire via l'objet request.

Voici un exemple qui récupère le nombre d'items de product

nb = int(request.POST.get("productitem_set-TOTAL_FORMS", "0"))