วันศุกร์ที่ 30 สิงหาคม พ.ศ. 2556

Django Admin Site Customization

1) enable customized Admin class

from django.contrib import admin

class QuestionAdmin(admin.ModelAdmin):
  pass

admin.site.register(Question, QuestionAdmin)

2) customize fields to display at list page

class QuestionAdmin(admin.ModelAdmin):
    # fields to display at list page
    list_display = ('id', 'subject', )
 
    # at list page, fields that when click will open individual edit page
    # NOTE: when edit from list page, also call save_model()
    # if save_model() depends on custom fields in custom form (which not exists in list page),
    # one work around is to call super class method to handle model save
    # see save_model() below.

    list_display_links = ['question_html']
 
    # fields to show at filter side bar
    list_filter = ('status', 'subject', 'source', 'year')
 
    # fields that can edit from list page
    list_editable = ('solution', 'question_type', 'point', 'status', )
 
    # - fields used for search when click search button
    # - can search across relationship, e.g. subject__name
    # - by default, search for objects contain case-insensitive specified keyword
    #   (i.e. in MySQL : WHERE source ILIKE '%Ent%')
    # - for faster search performance
    #   + prefix field name with '=' to search exact match, e.g. WHERE year='2555'
    #   + prefix field name with '^' to match beginning of the field

    search_fields = ['subject__name', 'source', '=year', 'tags__name', 'solution_html']

    # specified fields that are read-only
    # (enhance performance when display individual edit page)

    readonly_fields = ['created_time', 'student', 'question']

    # field used as date, and display as link in list page that when click can filter by that date field
    date_hierarchy = 'created_time'

    # specify default order when display list page
    ordering = ['-id']

    def save_model(self, request, obj, form, change):
        # if not change from QuestionAdminForm, use default save_model()
        if not isinstance(form, QuestionAdminForm):
            super(QuestionAdmin,self).save_model(request, obj, form, change)
            return


Some sample screens below.


2.1) display link in list page

by using a function in list_display, which return url and set allow_tags = True. 

class ModelAdmin(admin.ModelAdmin):
    list_display = ('id', 'view_link')
    def view_link(self, obj):
            q = Question.objects.get(node_id=obj.id)
            return u'<a href="%s">%s</a>' % (q.get_absolute_url(),q.name)
    view_link.allow_tags = True

2.2) filter data shown in list page

by overriding queryset() as shown below.

def queryset(self, request):
qs = super(NodeAdmin, self).queryset(request)
return qs.filter(type__in=[2, 3, 4])

2.3) collapse list filter sidebar (with jQuery)

detail here >> https://gist.github.com/abyx/1017597

class QuestionAdmin(admin.ModelAdmin):
    list_filter = ['activity_flag']

    class Media:
            js = ['/static/js/list_filter_collapse.js' ] # path to JavaScript file

content of list_filter_collapse.js

(function($){
ListFilterCollapsePrototype = {
    bindToggle: function(){
        var that = this;
        this.$filterTitle.click(function(){
            that.$filterContent.slideToggle();
            that.$list.toggleClass('filtered');
        });
    },
    init: function(filterEl) {
        this.$filterTitle = $(filterEl).children('h2');
        this.$filterContent = $(filterEl).children('h3, ul');
        $(this.$filterTitle).css('cursor', 'pointer');
        this.$list = $('#changelist');
        this.bindToggle();
    }
}
function ListFilterCollapse(filterEl) {
    this.init(filterEl);
}
ListFilterCollapse.prototype = ListFilterCollapsePrototype;

$(document).ready(function(){
    $('#changelist-filter').each(function(){
        var collapser = new ListFilterCollapse(this);
    });
});
})(django.jQuery);


3) use inline to display/edit/add related object

# subclass from admin.TabularInline
class SectionResourceInline(admin.TabularInline):
    # model used in this inline
    model = SectionResource
    # fields display in this inline
    fields = ('type', 'seq_no', 'node')
    # specify if to let user input id instead of select from drop-down list
    # to improve performance

    raw_id_fields = ['node']
    # extra row displayed to add new data
    extra = 1

4.1) use custom form to display/edit individual object

from django.contrib import admin

class QuestionAdminForm(forms.ModelForm):
    ...

    class Meta:
        model = Question

class QuestionAdmin(admin.ModelAdmin):
    form = QuestionAdminForm
 
    # group fields in custom Form
    # 1st element in tuple = verbose name
    # 2nd element in tuple = dict with grouped fields, each inner tuple are fields in same line
    # e.g. group 'Information' has 2 lines,
    # first line has field difficulty, second line has source, year, seq_no
    # can specify custom fields together with fields in Model (e.g. new_chapter below)

    fieldsets = (
        ('Content', {'fields': (('subject', 'chapter', 'new_chapter'), ), }),
        ('Information', {'fields': ('difficulty',
                                    ('source', 'year', 'seq_no'),
                                   ),
                        }),
    )
    ...


Sample screen here.

4.2) keep original field value to use when save

in custom form : override __init__() to keep original field value somewhere.
class NodeAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(NodeAdminForm, self).__init__(*args, **kwargs)

        # initiate values for custom fields if change_form
        node = kwargs.pop('instance', None)
        if node:
            self.old_size = node.size

    class Meta:
        model = Node
in admin class : refer to original field value in form when save object
class NodeAdmin(admin.ModelAdmin):
    form = NodeAdminForm
    def save_model(self, request, obj, form, change):
        if form.old_size != obj.size:
            # do something
        super(NodeAdmin, self).save_model(request, obj, form, change)

4.3) use custom fields in Form

class QuestionAdminForm(forms.ModelForm):
    # queryset : to get available choices
    # widget : use admin.widgets.FilteredSelectMultiple to display 2 boxes : available choices <--> selected choices
    #          2nd parameter : True if display 2 boxes vertically, False if display horizontally
    #                          rows in attrs is approximate size of widget?

    internal_label = forms.ModelMultipleChoiceField(label='Internal Label', required=False,queryset=Label.objects.filter(type=Label.INTERNAL_LABEL_TYPE).order_by('name'),widget= admin.widgets.FilteredSelectMultiple('Internal Label', False, attrs={'rows':'10'})
                       
                       

    # show as dropdown widget (single select)
    # use ModelMultipleChoiceField for multiple select dropdown

    chapter = forms.ModelChoiceField(queryset=Chapter.objects.all().order_by('name'), required=False)

    def __init__(self, *args, **kwargs):
        # to display other fields in model
        super(QuestionAdminForm, self).__init__(*args, **kwargs)

        # set required fields
        self.fields['field_name'].required = True
        ...

        # use RelatedFieldWidgetWrapper to add green PLUS sign besides FilteredSelectMultiple widget
        # detail here >> http://dashdrum.com/blog/2012/07/relatedfieldwidgetwrapper/
        # 1st parameter = widget to be wrapped
        # 2nd parameter = a relation that defines the two models involved
        # 3rd parameter = a reference to the admin site (assigned from Admin class' __init__())
        #                 QuestionLabel is a class that has ForeignKey to both Question and Label
        self.fields['internal_label'].widget = 
admin.widgets.RelatedFieldWidgetWrapper(self.fields['internal_label'].widget,
QuestionLabel._meta.get_field('label').rel, self.admin_site)
    class Meta:
        # model related to Form
        model = Question

        # specify special widget for field
        # AdminFileWidget is a widget to load file

        widgets = {'question_html': admin.widgets.AdminFileWidget, }


class QuestionAdmin(admin.ModelAdmin):
    form = QuestionAdminForm

    def __init__(self, model, admin_site):
        super(QuestionAdmin,self).__init__(model, admin_site)
     
        # capture the admin_site
        # for use with RelatedFieldWidgetWrapper in QuestionAdminForm
        self.form.admin_site = admin_site


Sample screen here.


4.4) initiate default value in custom Form

class QuestionAdminForm(forms.ModelForm):
    ...

    def __init__(self, *args, **kwargs):
        ...

        # get Model object from kwargs
        question = kwargs.pop('instance', None)
        if question:
            # for simple fields, simply set value
            self.question_html = question.question_html

            # for single select choice field, simply set selected object id
            self.initial['chapter'] = question.get_chapter_id()

            # for multiple select choice field,
            # create a dict with key = selected object id, value = True,
            # then set as initial value

            selected_internal_labels = {}
            for r in Label.objects.filter(...):
                selected_internal_labels[r.id] = True
            self.initial['internal_label'] = selected_internal_labels

4.5) validate input data (before calling Admin class' save_model())

NOTE: method call sequence : ModelForm.clean() > Model.clean() > ModelAdmin.save_model() > Model.save()
so validation can occur at ModelForm.clean() or Model.clean()

class QuestionAdminForm(forms.ModelForm):
    ...

    def clean(self):
        # default validation first
        cleaned_data = super(QuestionAdminForm, self).clean()

        # custom validation follows
        # raise a ValidationError and error message will be displayed at top of edit Form
        # ValidationError raised in Model.clean() also displayed in the same way

        if some_condition:
            raise ValidationError('subject mismatch')

        # don't forget to return cleaned data
        return cleaned_data

4.6) save model from custom Form

NOTE: method call sequence : ModelForm.clean() > Model.clean() > ModelAdmin.save_model() > Model.save()
so setting default value can be done at ModelAdmin.save_model() or Model.save()

class QuestionAdmin(admin.ModelAdmin):
    ...

    def save_model(self, request, obj, form, change):
        # call super class' save_model() to handle changes from list page
        # (if we use custom form with custom fields not in list page)

        if not isinstance(form, QuestionAdminForm):
            super(QuestionAdmin,self).save_model(request, obj, form, change)
            return

        # obj = model object to be saved
        # can set default value from request
        # NOTE: use request.POST.get() to avoid exception when key not exists (i.e. not input from Form)
        # NOTE: when get a list from request object, use request.POST.getlist('a_key_to_list_value'),
        #       not simply call request.POST.get()

        obj.created_by_id = request.user.id

        # can set default value from form data
        obj.question_html = form.question_html

        # call super class' save_model() method for default process
        super(QuestionAdmin,self).save_model(request, obj, form, change)

        # then some special process follows
        ...

        # call message_user() to display some messages
        self.message_user(request, 'done...')


5) specific action in Admin Form

detail here >> https://docs.djangoproject.com/en/1.5/ref/contrib/admin/actions/

class QuestionAdmin(admin.ModelAdmin):
    actions = ['my_action']

    # queryset contain objects selected by user from list page
    def my_action(self, request, queryset):
        # for each selected object, do something
        for r in queryset:
            do_something(r)
     
        # show some message after completed
        self.message_user(request, "successfully do something")
    # a verbose description to show in page
    my_action.short_description = 'Do something for some purpose'

ไม่มีความคิดเห็น:

แสดงความคิดเห็น