Package: django-baton-v2
django-batonA cool, modern and responsive django admin application, based on bootstrap 4.3.1Documentation:readthedocsTable of contentsFeaturesInstallationConfigurationMenuAnalyticsSignalsText Input FiltersForm TabsCustomizationContributingScreenshotsFeaturesTested with django 1.10.5, 1.11.1, 2.0, 2.1 and 2.2This application was written with one concept in mind: overwrite as few django templates as possible.
Everything is styled through css, and when an help is needed, js is armed.Based on bootstrap 4.3.1 and FontAwesome Free 5.8.1Fully responsiveCustom and flexible sidebar menuText input filters facilityForm tabs out of the boxLazy load of current uploaded imagesOptional index page filled with google analytics widgetsCustomization available recompiling the js app providedit translations providedIt requires the following python packages in order to manage the google analytics index:google-api-python-clientoauth2client==1.5.2At the momentbatondefines only 3 custom templates:admin/base_site.html, needed in order to inject the js application (which includes also css and graphics, compiled withwebpack);admin/delete_confirmation.html, needed because of a bug (IMO) in the template, in particular theextra_headblock does not contain the parent content, hence it must be overridden (FIXED IN django 1.11, remains until baton will support django 1.10);admin/delete_selected_confirmation.html, same as above.Baton is based on the following frontend technologies:bootstrap 4.3.1FontAwesome 5.8.1 (solid and brands)Flexbox is used here and there to accomplish responsiveness. jQuery is used for DOM manipulations.All js, fonts and css are compiled, and produce a single js file which is included in the base_site template.A custom menu is provided, the menu is rendered through js, and data are fetched in json format through ajax request.InstallationInstall the last stable releasepip install django-batonor clone the repo inside your projectgit clone https://github.com/otto-torino/django-baton.gitAddbatonandbaton.autodiscoverto yourINSTALLED_APPS:INSTALLED_APPS = (
    # ...
    'baton',
    'django.contrib.admin',
    # ...
    'baton.autodiscover',
)Replace django.contrib.admin in your project urls, and add baton urls:# from django.contrib import admin
from baton.autodiscover import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    # ...
    url(r'^baton/', include('baton.urls')),
]Django 2The first two steps are the same, but in your project urls you should now use path:from baton.autodiscover import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('baton/', include('baton.urls')),

]If you get a "No crypto library available" when using the google analytics index, then install this package:$ pip install PyOpenSSLWhy two installed apps?Well, the firstbatonhas to be placed before thedjango.contrib.adminapp, because it overrides 3 templates and resets all css.
Thebaton.autodiscoverentry is needed as the last installed app in order to register all applications for the admin.
I decided to create a customAdminSiteclass, in order to allow the customization of some variables the django way (site_header,index_title, ...). I think this is a good approach,
better than customizing this vars overwriting the orignal templates. The problem is that when creating a custom AdminSite, you should register manually all the apps. I didn't like
this, so I wrote thisautodiscovermodule, which automatically registers all the apps already registered with the django default AdminSite. In order to do this, all the apps must be already registered, so it comes as the last installed app.ConfigurationThe configuration dictionary must be defined inside your settings:BATON = {
    'SITE_HEADER': 'Baton',
    'SITE_TITLE': 'Baton',
    'INDEX_TITLE': 'Site administration',
    'SUPPORT_HREF': 'https://github.com/otto-torino/django-baton/issues',
    'COPYRIGHT': 'copyright Â© 2017 <a href="https://www.otto.to.it">Otto srl</a>', # noqa
    'POWERED_BY': '<a href="https://www.otto.to.it">Otto srl</a>',
    'CONFIRM_UNSAVED_CHANGES': True,
    'SHOW_MULTIPART_UPLOADING': True,
    'ENABLE_IMAGES_PREVIEW': True,
    'MENU': (
        { 'type': 'title', 'label': 'main', 'apps': ('auth', ) },
        {
            'type': 'app',
            'name': 'auth',
            'label': 'Authentication',
            'icon': 'fa fa-lock',
            'models': (
                {
                    'name': 'user',
                    'label': 'Users'
                },
                {
                    'name': 'group',
                    'label': 'Groups'
                },
            )
        },
        { 'type': 'title', 'label': 'Contents', 'apps': ('flatpages', ) },
        { 'type': 'model', 'label': 'Pages', 'name': 'flatpage', 'app': 'flatpages' },
        { 'type': 'free', 'label': 'Custom Link', 'url': 'http://www.google.it', 'perms': ('flatpages.add_flatpage', 'auth.change_user') },
        { 'type': 'free', 'label': 'My parent voice', 'children': [
            { 'type': 'model', 'label': 'A Model', 'name': 'mymodelname', 'app': 'myapp' },
            { 'type': 'free', 'label': 'Another custom link', 'url': 'http://www.google.it' },
        ] },
    ),
    'ANALYTICS': {
        'CREDENTIALS': os.path.join(BASE_DIR, 'credentials.json'),
        'VIEW_ID': '12345678',
    }
}SITE_HEADER,COPYRIGHTandPOWERED_BYare marked as safe, so you can include also img tags and links, for example.SUPPORT_HREFis the content of an href attribute, then you can use also amailto:info@blabla.comCONFIRM_UNSAVED_CHANGES: if set toTruea confirmation modal appears when leaving a change form or add form with unsaved changes.
The check of a dirty form relies on the jQuery serialize method, so it's not 100% safe. Disabled inputs, particular widgets (ckeditor) can not be detected.
Default value isTrue.SHOW_MULTIPART_UPLOADING: if set toTruean overlay with a spinner appears when submitting amultipart/form-dataform.ENABLE_IMAGES_PREVIEW: if set toTruea preview is displayed above all input file fields which contain images. You can control how the preview is displayed overriding the class.baton-image-preview. By default previews are 100px height and with a box shadow on over event.Let's see theMENUandANALYTICSconfigurations in detail.MENUCurrently four kind of voices are supported:title,app,modelandfree.Title and free voices can have children, children follow the following rules:children voices children are ignored (do not place an app voice as child)First of all, if you don't define a MENU key in the configuration dictionary, the default MENU is shown.
If you define a MENU key, then the custom menu is built and shown.TitleLikeMAINandCONTENTSin the screenshot, it represents a menu section. You should set a label and optionally an apps or perms key, used for visualization purposes.If the title voice should act as a section title for a group of apps, you'd want to specify these apps, because if the user can't operate over them, then the voice is not shown.
At the same time you can define some perms (OR condition), something like:{ 'type': 'title', 'label': 'main', 'perms': ('auth.add_user', ) },AppYou must specify thetypeandnamekeys, optionally an icon key (you can use FontAwesome classes which are included by default) and amodelskey.
If you don't define themodelskey then the default app models are listed under your app, otherwise only the specified models are listed (in the order you provide).ModelYou must specify thetype,nameandappkeys, optionally an icon key (you can use FontAwesome classes which are included by default).FreeYou can specify free voices, you must define anurland if you want some visibility permissions (OR clause)ANALYTICSYou can create a cool index page displaying some statistics widgets consuming the google analytics api, just by defining theANALYTICSsetting.It requires two keys:CREDENTIALS: it is the path to the credentials json fileVIEW_ID: id of the view from which display dataYou can add contents before and after the analytics dashboard by extending thebaton/analytics.htmltemplate and filling thebaton_before_analyticsandbaton_after_analyticsblocks.How to generate a credentials json fileFollow the steps in the Google Identity Platform documentation tocreate a service accountfrom theGoogle Developer Console.Once the service account is created, you can click the Generate New JSON Key button to create and download the key and add it to your project.Add the service account as a user in Google Analytics. The service account you created in the previous step has an email address that you can add to any of the Google Analytics views you'd like to request data from. It's generally best to only grant the service account read-only access.SignalsBaton provides a dispatcher that can be used to register function that will be called when some events occurr.
At this moment Baton emits four types of events:onNavbarReady: dispatched when the navbar is fully renderedonMenuReady: dispatched when the menu is fully rendered (probably the last event fired, since the menu contents are retrieves async)onTabsReady: dispatched when the changeform tabs are fully renderedonMenuError: dispatched if the request sent to retrieve menu contents failsonReady: dispatched when Baton js has finished its sync jobIn order to use them just override the batonadmin/base_site.htmltemplate and register your listenersbeforecallingBaton.init, i.e.<!-- ... -->
<script>
    {% baton_config 'CONFIRM_UNSAVED_CHANGES' as confirm_unsaved_changes %}
    {% baton_config 'SHOW_MULTIPART_UPLOADING' as show_multipart_uploading %}
    (function ($, undefined) {
        $(window).on('load', function () {
            // init listeners
            Baton.Dispatcher.register('onReady', function () { console.log('BATON IS READY') })
            Baton.Dispatcher.register('onMenuReady', function () { console.log('BATON MENU IS READY') })
            Baton.Dispatcher.register('onNavbarReady', function () { console.log('BATON NAVBAR IS READY') })
            // end listeners
            Baton.init({
                api: {
                    app_list: '{% url 'baton-app-list-json' %}'
                },
                confirmUnsavedChanges: {% if confirm_unsaved_changes %}true{% else%}false{% endif %},
                showMultipartUploading: {% if show_multipart_uploading %}true{% else%}false{% endif %}
            });
        })
    })(jQuery, undefined)
</script>
<!-- ... -->Text Input FiltersTaken from thismedium articleBaton defines a custom InputFilter class that you can use to create text input filters and use them as any otherlist_filters, for example# your app adminfrombaton.adminimportInputFilterclassIdFilter(InputFilter):parameter_name='id'title='id'defqueryset(self,request,queryset):ifself.value()isnotNone:search_term=self.value()returnqueryset.filter(id=search_term)classMyModelAdmin(admin.ModelAdmin):list_filters=('my_field',IdFilter,'my_other_field',)Form tabsHow much I loved django-suit form tabs? Too much. So, this was a feature I couldn't live without.There are three types of tabs:fieldset tab: a tab containing a fieldsetinline tab: a tab containing an inlinegroup tab: a tab which can contain fieldsets and inlines in the order you specifyTabs' titles are retrieved automatically, for fieldset and inline tabs are the fieldset title and the inline related verbose name plural.
For group tabs the first title is taken (either of an inline or fieldset section).Using group tabs you can mix inlines with fields, just by splitting fields into fieldsets and arranging them in your preferred order.Let's see how to define tabs in your admin forms (everyting is done through js, no templatetags, no templates overriden):class AttributeInline(admin.StackedInline):
    model = Attribute
    extra = 1

class FeatureInline(admin.StackedInline):
    model = Feature
    extra = 1

class ItemAdmin(admin.ModelAdmin):
    list_display = ('label', 'description', 'main_feature', )
    inlines = [AttributeInline, FeatureInline, ]

    fieldsets = (
        ('Main', {
            'fields': ('label', ),
            'classes': ('baton-tabs-init', 'baton-tab-inline-attribute', 'baton-tab-fs-content', 'baton-tab-group-fs-tech--inline-feature', ),
            'description': 'This is a description text'

        }),
        ('Content', {
            'fields': ('text', ),
            'classes': ('tab-fs-content', ),
            'description': 'This is another description text'

        }),
        ('Tech', {
            'fields': ('main_feature', ),
            'classes': ('tab-fs-tech', ),
            'description': 'This is another description text'

        }),
    )As you can see these are the rules:Inline classes remain the same, no action neededIn the first fieldset define abaton-tabs-initclass which enables tabsFor every InLine you want to put in a separate tab, add a classbaton-tab-inline-MODELNAMEorbaton-tab-inline-RELATEDNAMEif you've specified a related name in the model foreign key fieldFor every fieldset you want to put in a separate tab, add a classbaton-tab-fs-CUSTOMNAME, and add a classtab-fs-CUSTOMNAMEon the fieldsetFor every group you want to put in a separate tab, add a classbaton-tab-group-ITEMS, where items can be inlines (inline-RELATEDNAME) and/or fieldsets (fs-CUSTOMNAME) separated by a double hypen--. Also add a classtab-fs-CUSTOMNAMEon the fieldset items.Tabs order respects the defined classes orderOther features:when some field has an error, the first tab containing errors is opened automaticallyyou can open a tab on page load just by adding an hash to the url, i.e.#inline-feature,#fs-content,#group-fs-tech--inline-featureCustomizationIt's easy to heavily customize the appeareance ofbaton. All the stuff is compiled from a modern js app which resides inbaton/static/baton/app.You just need to change thesass variables values(and you can overwrite also bootstrap variables), re-compile, get the compiled js file, place it in the static folder of your main app,
and place your main app (ROOTAPP) beforebatonin theINSTALLED_APPS.So:$ git clone https://github.com/otto-torino/django-baton.git
$ cd django-baton/baton/static/baton/app/
$ npm install
$ vim src/styles/_variables.scss
$ npm run compile
$ cp dist/baton.min.js ROOTAPP/static/baton/app/dist/If you want to test live your changes it's easy, just start the webpack dev server:$ cd django-baton/baton/static/baton/app/
$ npm run devAnd the inside thebase_site.htmltemplate make these changes:<!-- <script src="{% static 'baton/app/dist/baton.min.js' %}"></script> comment the compiled src and uncomment the webpack served src -->
<script src="http://localhost:8080/dist/baton.min.js"></script>Now while you make your changes to the js app (css included), webpack will update the bundle automatically, so just refresh the page and you'll see your changes.ContributingI'll soon add more stuff here, but at the moment what is really important, is to follow the eslint rules specified in the.eslintrcfile (https://github.com/otto-torino/django-baton/blob/master/baton/static/baton/app/.eslintrc) for the js part, and be compliant with the standard sasslint rules for the sass part. I follow PEP8 standard for the few lines of python code.Screenshots