วันเสาร์ที่ 31 สิงหาคม พ.ศ. 2556

Display Progress for Long Process

Situation

- upload file from a HTML form with submit button
- after submit, processing file take too long and timeout occur
- want to display progress after submit request

Solution 1

- use thread to process request, thus return response immediately
- keep progress data in session
- use Ajax to submit and display progress
Drawback
- no progress shown during submit big file

Solution 2

- use submit button to submit request
- use thread to process request, thus return response immediately
- keep progress data in session
- redirect to progress page, which use Ajax to display progress

Detail for Solution : use thread to process request

- for uwsgi, enable thread in uwsgi.ini by adding this row
    # enable thread
    enable-threads=True
- in Django, use code below to create thread
    import threading
    ...

    def func():
        ...
        args = [arg1, arg2]
        t = threading.Thread(target=do_something, args=args)
        t.daemon = True
        t.start()

    def do_something(arg1, arg2):
        ...


Detail for Solution : use Ajax to submit form data

- use jQuery Form Plugin to submit form data (especially uploaded file) from ajax
  detail here >> http://jquery.malsup.com/form/#getting-started
  detail for available options >> http://jquery.malsup.com/form/#options-object
    options = {
        type: "POST",
        url: "/import_ajax", // target url to submit form data
        validate: true,
        data: {'validate_question_ajax': '1'}, // additional data to be submitted with other form data
        dataType: 'json',
        success: function(response, textStatus, jqXHR){
            ...
            // start timer (to display progress) only if request is submitted successfully             init_timer();
        },
        // callback handler that will be called on error

        error: function(jqXHR, textStatus, errorThrown){
            ...
        }
    };
    $('#MyForm').ajaxSubmit(options);

Detail for Solution : save progress in session

- code below in django to get/set progress in session
    // enforce no cache for progress
    // redirect to this method from url, e.g. "/import_progress"

    @cache_control(no_cache=True)
    def get_progress(request):
        // use session key in request to get session data

        s = SessionStore(session_key=request.session._session_key)
        progress = s.get('progress', None)
        is_done = s.get('is_done', None)
        if is_done:
            return JSONResponse({'message': 'done %s records.' % progress, 'detail': s['detail']})
        else:
            return JSONResponse({'message': 'process %s records.' % progress, })

    def set_progress(session_key, progress, detail=None, reset=False):
        s = SessionStore(session_key=session_key)
        s['progress'] = progress
        s['is_done'] = (detail is not None)
        if detail:
            s['detail'] = detail
        elif reset:
            s['detail'] = []
        s.save()

    def do_something(request):
        // reset progress data immediately before process request

        set_progress(request.session._session_key, 0, reset=True)

Detail for Solution : use Ajax to display progress

- code below in javascript
    var progress_timer;

    // a function to initialize timer (called later)

    function init_timer() {
        // set timer to update status of importing Slam history

        progress_timer = setInterval(function() {
            $.ajax({
                url: "/import_progress", // url that return progress data, redirect to method get_progress() above
                ...
                success: function(response, textStatus, jqXHR){
                    // output progress

                    $("#import_progress").html(response.message);
                    if (response.is_done != null) {
                        // stop timer when process completed

                        clearInterval(progress_timer);
                    }
                    // emergency braker

                    i += 1;
                    if (i > 1000) {
                        clearInterval(progress_timer);
                    }
                },
                ...
            });
        }, 5000); // timer interval in millisec
    }

    ...

        // pass this as ajax submit option
        // this will be returned immediately after worker thread started

        success: function(response, textStatus, jqXHR){
            if (response.error != null) {
                ...
            }
            else {
                // if worker thread started normally, start timer to display progress
                init_timer();
            }
        },

Detail Solution : HTML page to show progress

- code below in javascript to initialize timer when loaded page
    $(function() {
        jQuery(document).ready(function(e) {
            init_timer(); // same function as above
        });
    });

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

How to MySQLTuner

http://www.ovaistariq.net/358/tuning-mysql-server-settings/
http://www.ovaistariq.net/496/tuning-innodb-configuration/


http://dev.mysql.com/doc/refman/5.5/en/server-parameters.html
When tuning a MySQL server, the two most important variables to configure are key_buffer_size andtable_open_cache. You should first feel confident that you have these set appropriately before trying to change any other variables.


http://www.howtoforge.com/tuning-mysql-performance-with-mysqltuner

http://rtcamp.com/wordpress-nginx/tutorials/mysql/mysqltuner/
1. Run mysqltuner after 24 hours. If you don't, it will remind you by showing “MySQL started within last 24 hours ? recommendations may be inaccurate.” Reason: mysqltuner recommendation may prove inaccurate.

2. If it asks you to change value of tmp_table_size or max_heap_table_size variable, make sure you change both and keep them equal. These are global values so feel free to increase them by large chunks (provided you have enough memory on server)

3. If it asks you to tweak join_buffer_size, tweak in small chunks as it will be multiplied by value of max_connections.

4. If it asks you to increase innodb_buffer_pool_size, make it large. Ideally, it should be large enough to accommodate your all innodb databases. If you do not have enough RAM consider buying some. Otherwise try to delete unwanted database. Do not ignore this as it can degrade performance significantly.

5. Try to keep maximum possible memory less than 50%. Other lines can tell you, if your site is using too “less” mysql connections. In that case, you can reduce max_connections and increase other buffers more generously.

6. Also, whenever you make changes to mysql config and restart mysql server, always run mysqltuner immediately to check if by mistake you haven’t made maximum possible memory usage too high! Ignore any other suggestion it will give for next 24-hours!

7. As we use mysqltuner many times, it will be convenient to create hidden .my.cnf file in your home-dir. Do not confuse this with mysql-server’s my.cnf file.
Create file ~/.my.cnf and add following lines in it and replace mysqluser & mysqlpass values.
[client]
user=mysqluser
pass=mysqlpass
For safety, make this file readable to you only by running chmod 0600 ~/.my.cnf
Now on next run, mysqltuner will not ask you for password.

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'