AJAX

Overview

We currently use AJAX to:

  • Add data
  • Delete data
  • Edit data
  • View data that was just edited or added

These actions are put into the HTML using input elements. The onClick attribute of these input call javascript methods.

The goal of the following code is to provide a framework so that adding more AJAX of this kind requires minimal effort:

  • Add input elements into the html for adding, deleting or editing a chunk of data, d
  • If no template exists in app/templates/models/ for displaying d add one
  • If no form exists in app/views/forms for editing d and one

In terms of security, all python methods that change data are decorated so that even when malicious users corrupt the javascript, data access and modification is still secure.

HTML

  • o is some object, eg an Action for the creation of a Node`
  • mname is the model name o the object, eg Node
    <SMALL>
            <INPUT
                    TYPE="button"
                    onClick="insert_form('{% url ajax_edit mname,o.id %}', '{{mname}}_{{o.id}}', 'models/action_item.html');"
                    VALUE="edit"
                    class="ajax_button edit fright"
            />
            <INPUT
                    TYPE="button"
                    onClick="delete_item('{% url ajax_delete mname,o.id %}', '{{mname}}_{{o.id}}', 'models/action_item.html');"
                    VALUE="delete"
                    class="ajax_button delete fright"
            />
    </SMALL>

Javascript

/// The following functions are helpers for replacing a html element's contents with a form,
/// and then putting the element's contents (with any data updates) back.

///
/// GET to a form URL and insert returned form html into container.
/// eg: insert_form('/Main/node/edit/2221', 'node_2221', 'graph/node_brief.html');
/// 'template' is the template that is currently rendered in the container 
///     and which will be replaced by the insert.
///     Once the form is submitted or canceled, 'template' will again be rendered.
/// 'extra' is a javascript object containing extra data to add to the GET parameters.
///     This is useful for getting new forms that need hidden fields to be set.
///
function new_form(url, container, template, extra) {
        var u = url+'?container='+container+'&template='+template;
        for (var key in extra) {
                u = u + '&' + key + '=' + extra[key];
        }
        $("#"+container).load(
                u,
                {},
                function (responseText, textStatus, XMLHttpRequest) {
                        textarea_to_rte(container);
                }
        );
}
///
/// POST form and insert returned form html (with errors) or returned template into container.
/// The form's save button should use this; eg:
///     replace_form('/Main/node/edit/2221', 'node_2221', 'graph/node_brief.html');
///
function replace_form(url, container, template) {
        if (url.indexOf('?') == -1) {
                url = url+'?container='+container+'&template='+template;
        }
        $("#"+container+" .rte_disable").click(); 
        $("#"+container).load(
                url,
                $(':input').serializeArray(),
                function (responseText, textStatus, XMLHttpRequest) {
                        textarea_to_rte(container);
                }
        );
}


///
/// Do not POST, instead return template to container. Template may have changed if someone else changed the data.
/// The form's cancel button should use this; eg:
///     cancel_form('/Main/node/cancel/2221', 'node_2221', 'graph/node_brief.html');
///
function cancel_form(cancel_url, container, template) {
        $("#"+container).load(
                        cancel_url+'?container='+container+'&template='+template
        );
}

///
/// Deletes an item and rerenders the container with the specified template.
/// This allows the container to display ITEM WAS DELETED or something.
///     delete_item('/Main/node/delete/2221', 'node_2221', 'graph/node_brief.html');
///
function delete_item(delete_url, container, template) {
        var txt = "Are you sure you want to delete this item?\n\nIf you have any questions, please ask first!\n\nClick OK to delete this item.";
        if (delete_url.indexOf('?') == -1) {
                delete_url = delete_url+'?container='+container+'&template='+template;
        }
        if (confirm(txt)) {
                // show alert. if user clicks OK then...
                $("#"+container).load(
                        delete_url
                );
        } else {
                // nothing.
        }
}

///
/// Helper that does textarea to rich-text-editor conversion.
///
function textarea_to_rte(container) {
        $("#"+container+" textarea").wrap("<DIV class=\"rte_zone_background\"></DIV>");
        $("#"+container+" textarea").addClass("rte_zone").rte("/Main/media/css/rte.css");
}

Views

@ajax(superuser)
def edit_object(request, model, model_id):
    """
    Returns a form for editing the model instance with model_id. If form is 
    valid, renders instance in 'template' extracted from GET. Also extracts
    'container' from GET to pass to cancel url.

    @param model: model class name
    @param model_id: model object id
    @param GET template
    @param GET container
    """
    Model = globals()[model]
    ModelForm = globals()[model+'Form']
    container = request.REQUEST['container']
    template = request.REQUEST['template']
    m = Model.get(int(model_id))
    if request.POST:
        form = ModelForm(request.POST, instance=m, prefix=model+model_id)
        if form.is_valid():
            form.save()
            return json_response(render_string(request, template, { model.lower(): m, 'object': m, 'o': m, 'mname': model }))
    else:
        form = ModelForm(instance=m, prefix=model+model_id)
    action_url = request.get_full_path()
    cancel_url = reverse('ajax_cancel_edit', args=(model, model_id))+'?template='+template
    return json_response(render_string(request, 'forms/object_form.html', locals()))

@ajax(logged_in)
def new_object(request, model):
    """
    Returns a form for editing a new instance of model. Any extra/default data
    to give form must be passed in GET parameters. If form is valid, renders 
    instance in 'template' extracted from GET. Also extracts 'container' from 
    GET to pass to cancel url.

    @param model: model class name
    @param GET template
    @param GET container
    @param GET extra data for form
    """
    Model = globals()[model]
    ModelForm = globals()['New%sForm' % model]
    container = request.REQUEST['container']
    template = request.REQUEST['template']
    data = {}
    if model == 'Behavior':
        n = Node.get(int(request.REQUEST['node_id']))
        m = Behavior(node=n)
    elif model == 'Node':
        m = Node()
    if request.POST:
        form = ModelForm(request.POST, instance=m, prefix=model+'new')
        if form.is_valid():
            m = form.save()
            return json_response(render_string(request, template, { model.lower(): m, 'object': m, 'o': m, 'mname': model }))
    else:
        form = ModelForm(instance=m, prefix=model+'new')
    action_url = request.get_full_path()
    cancel_url = reverse('ajax_cancel_edit', args=(model, 'new'))+'?'+request.GET.urlencode()
    return json_response(render_string(request, 'forms/object_form.html', locals()))

@ajax(superuser)
def delete_object(request, model, model_id):
    """
    Deletes instance specified by model and model_id. Renders 
    instance in 'template' extracted from GET. 

    @param model: model class name
    @param model_id: model instance id
    @param GET template
    @param GET container
    """
    Model = globals()[model]
    ModelForm = globals()[model+'Form']
    m = Model.get(int(model_id))
    container = request.REQUEST['container']
    template = request.REQUEST['template']
    #### TODO: don't delete, just change state
    ####       or actually delete but also serialize into delete action so could recreate.
    #### for now, delete item and action
    m.action.delete()
    m.delete()
    return json_response(render_string(request, template, {}))

@ajax(superuser)
def cancel_edit_object(request, model, model_id):
    """
    Renders instance specified by model and model_id in 'template' extracted from GET.
    If canceling a form for a new model instance, then model_id should equal 'new'. 

    @param model: model class name
    @param model_id: model instance id or 'new' if canceling a form for a new model instance
    @param GET template
    """
    template = request.REQUEST['template']
    Model = globals()[model]
    if model_id == 'new':
        return json_response(render_string(request, template, request.GET))
    else:
        m = Model.get(int(model_id))
        return json_response(render_string(request, template, { model.lower(): m, 'object': m, 'o': m, 'mname': model }))