diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index fe1a64f5cf..582123b78f 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,7 +1,6 @@ from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.modulestore.draft import DRAFT from xmodule.modulestore.exceptions import ItemNotFoundError @@ -35,13 +34,14 @@ def get_course_location_for_item(location): return location -def get_lms_link_for_item(location): +def get_lms_link_for_item(location, preview=False): location = Location(location) if settings.LMS_BASE is not None: - lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( + lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format( + preview='preview.' if preview else '', lms_base=settings.LMS_BASE, # TODO: These will need to be changed to point to the particular instance of this problem in the particular course - course_id = modulestore().get_containing_courses(location)[0].id, + course_id=modulestore().get_containing_courses(location)[0].id, location=location, ) else: @@ -77,5 +77,4 @@ def compute_unit_state(unit): def get_date_display(date): - print date, type(date) return date.strftime("%d %B, %Y at %I:%M %p") diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index ef9613581b..93f867549c 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -10,6 +10,7 @@ import sys import time import tarfile import shutil +from datetime import datetime from collections import defaultdict from uuid import uuid4 from lxml import etree @@ -30,6 +31,7 @@ from django import forms from django.shortcuts import redirect from xmodule.modulestore import Location +from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.x_module import ModuleSystem from xmodule.error_module import ErrorDescriptor from xmodule.errortracker import exc_info_to_str @@ -40,6 +42,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string from xmodule.modulestore.django import modulestore from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule.exceptions import NotFoundError +from xmodule.timeparse import parse_time, stringify_time from functools import partial from itertools import groupby from operator import attrgetter @@ -106,9 +109,10 @@ def index(request): courses = modulestore().get_items(['i4x', None, None, 'course', None]) # filter out courses that we don't have access to - courses = filter(lambda course: has_access(request.user, course.location), courses) + courses = filter(lambda course: has_access(request.user, course.location) and course.location.course != 'templates', courses) return render_to_response('index.html', { + 'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'courses': [(course.metadata.get('display_name'), reverse('course_index', args=[ course.location.org, @@ -188,6 +192,7 @@ def edit_subsection(request, location): break lms_link = get_lms_link_for_item(location) + preview_link = get_lms_link_for_item(location, preview=True) # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': @@ -207,14 +212,13 @@ def edit_subsection(request, location): policy_metadata = dict((key,value) for key, value in item.metadata.iteritems() if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) - logging.debug(policy_metadata) - return render_to_response('edit_subsection.html', {'subsection': item, 'context_course': course, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'lms_link': lms_link, - 'parent_item' : parent, + 'preview_link': preview_link, + 'parent_item': parent, 'policy_metadata' : policy_metadata }) @@ -240,8 +244,8 @@ def edit_unit(request, location): course.location.course == item.location.course): break - # The non-draft location - lms_link = get_lms_link_for_item(item.location._replace(revision=None)) + lms_link = get_lms_link_for_item(item.location) + preview_lms_link = get_lms_link_for_item(item.location, preview=True) component_templates = defaultdict(list) @@ -282,9 +286,10 @@ def edit_unit(request, location): 'unit_location': location, 'components': components, 'component_templates': component_templates, - 'draft_preview_link': lms_link, + 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, + 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else 'Unset', 'section': containing_section, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'unit_state': unit_state, @@ -480,20 +485,13 @@ def _xmodule_recurse(item, action): _xmodule_recurse(child, action) action(item) - -def _delete_item(item, recurse=False): - if recurse: - children = item.get_children() - for child in children: - _delete_item(child, recurse) - - modulestore().delete_item(item.location); @login_required @expect_json def delete_item(request): item_location = request.POST['id'] + item_loc = Location(item_location) # check permissions for this user within this course if not has_access(request.user, item_location): @@ -501,16 +499,28 @@ def delete_item(request): # optional parameter to delete all children (default False) delete_children = request.POST.get('delete_children', False) + delete_all_versions = request.POST.get('delete_all_versions', False) item = modulestore().get_item(item_location) + store = _modulestore(item_loc) - # @TODO: this probably leaves draft items dangling. + + # @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be + # if item.location.revision=None, then delete both draft and published version + # if caller wants to only delete the draft than the caller should put item.location.revision='draft' if delete_children: - _xmodule_recurse(item, lambda i: _modulestore(i.location).delete_item(i.location)) + _xmodule_recurse(item, lambda i: store.delete_item(i.location)) else: - _modulestore(item.location).delete_item(item.location) + store.delete_item(item.location) + + # cdodge: this is a bit of a hack until I can talk with Cale about the + # semantics of delete_item whereby the store is draft aware. Right now calling + # delete_item on a vertical tries to delete the draft version leaving the + # requested delete to never occur + if item.location.revision is None and item.location.category=='vertical' and delete_all_versions: + modulestore('direct').delete_item(item.location) return HttpResponse() @@ -524,13 +534,15 @@ def save_item(request): if not has_access(request.user, item_location): raise PermissionDenied() + store = _modulestore(Location(item_location)); + if request.POST['data']: data = request.POST['data'] - modulestore().update_item(item_location, data) + store.update_item(item_location, data) if request.POST['children']: children = request.POST['children'] - modulestore().update_children(item_location, children) + store.update_children(item_location, children) # cdodge: also commit any metadata which might have been passed along in the # POST from the client, if it is there @@ -560,7 +572,7 @@ def save_item(request): existing_item.metadata.update(posted_metadata) # commit to datastore - modulestore().update_metadata(item_location, existing_item.metadata) + store.update_metadata(item_location, existing_item.metadata) return HttpResponse() @@ -845,9 +857,9 @@ def asset_index(request, org, course, name): display_info['url'] = StaticContent.get_url_path_from_location(asset_location) # note, due to the schema change we may not have a 'thumbnail_location' in the result set - thumbnail_location = Location(asset.get('thumbnail_location', None)) - - display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) + _thumbnail_location = asset.get('thumbnail_location', None) + thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None + display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None asset_display.append(display_info) @@ -863,6 +875,43 @@ def asset_index(request, org, course, name): def edge(request): return render_to_response('university_profiles/edge.html', {}) +@login_required +@expect_json +def create_new_course(request): + template = Location(request.POST['template']) + org = request.POST.get('org') + number = request.POST.get('number') + display_name = request.POST.get('display_name') + + dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) + + # see if the course already exists + existing_course = None + try: + existing_course = modulestore('direct').get_item(dest_location) + except ItemNotFoundError: + pass + + if existing_course is not None: + return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'})) + + new_course = modulestore('direct').clone_item(template, dest_location) + + if display_name is not None: + new_course.metadata['display_name'] = display_name + + # we need a 'data_dir' for legacy reasons + new_course.metadata['data_dir'] = uuid4().hex + + # set a default start date to now + new_course.metadata['start'] = stringify_time(time.gmtime()) + + modulestore('direct').update_metadata(new_course.location.url(), new_course.own_metadata) + + create_all_course_groups(request.user, new_course.location) + + return HttpResponse(json.dumps({'id': new_course.location.url()})) + @ensure_csrf_cookie @login_required def import_course(request, org, course, name): @@ -929,4 +978,8 @@ def import_course(request, org, course, name): return render_to_response('import.html', { 'context_course': course_module, 'active_tab': 'import', + 'successful_import_redirect_url' : reverse('course_index', args=[ + course_module.location.org, + course_module.location.course, + course_module.location.name]) }) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index faf7dc8886..e29ee62e20 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -53,7 +53,7 @@ DATABASES = { } } -LMS_BASE = "http://localhost:8000" +LMS_BASE = "localhost:8000" REPOS = { 'edx4edx': { diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 8602770d24..b0cb73cb50 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -32,9 +32,11 @@ $(document).ready(function() { $('.save-subsection').bind('click', saveSubsection); // making the unit list sortable - $('.sortable-unit-list').sortable(); - $('.sortable-unit-list').disableSelection(); - $('.sortable-unit-list').bind('sortstop', onUnitReordered); + $('.sortable-unit-list').sortable({ + axis: 'y', + handle: '.drag-handle', + update: onUnitReordered + }); // expand/collapse methods for optional date setters $('.set-date').bind('click', showDateSetter); @@ -58,6 +60,23 @@ $(document).ready(function() { e.preventDefault(); $('.import .file-input').click(); }); + + // Subsection reordering + $('.unit-list ol').sortable({ + axis: 'y', + handle: '.section-item .drag-handle', + update: onSubsectionReordered + }); + + // Section reordering + $('.courseware-overview').sortable({ + axis: 'y', + handle: 'header .drag-handle', + update: onSectionReordered + }); + + $('.new-course-button').bind('click', addNewCourse); + }); function showImportSubmit(e) { @@ -65,6 +84,7 @@ function showImportSubmit(e) { $('.file-name-block').show(); $('.import .choose-file-button').hide(); $('.submit-button').show(); + $('.progress').show(); } function syncReleaseDate(e) { @@ -110,12 +130,7 @@ function onUnitReordered() { var subsection_id = $(this).data('subsection-id'); var _els = $(this).children('li:.leaf'); - - var children = new Array(); - for(var i=0;i<_els.length;i++) { - el = _els[i]; - children[i] = $(el).data('id'); - } + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // call into server to commit the new order $.ajax({ @@ -127,6 +142,38 @@ function onUnitReordered() { }); } +function onSubsectionReordered() { + var section_id = $(this).data('section-id'); + + var _els = $(this).children('li:.branch'); + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : section_id, 'metadata' : null, 'data': null, 'children' : children}) + }); +} + +function onSectionReordered() { + var course_id = $(this).data('course-id'); + + var _els = $(this).children('section:.branch'); + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : course_id, 'metadata' : null, 'data': null, 'children' : children}) + }); +} + function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { var input_date = $('#'+date_id).val(); var input_time = $('#'+time_id).val(); @@ -241,7 +288,7 @@ function _deleteItem($el) { var id = $el.data('id'); $.post('/delete_item', - {'id': id, 'delete_children' : true}, + {'id': id, 'delete_children' : true, 'delete_all_versions' : true}, function(data) { $el.remove(); }); @@ -306,7 +353,7 @@ function hideModal(e) { function onKeyUp(e) { if(e.which == 87) { - $body.toggleClass('show-wip'); + $body.toggleClass('show-wip hide-wip'); } } @@ -406,6 +453,7 @@ function addNewSection(e) { $newSection.find('.new-section-name-cancel').bind('click', cancelNewSection); } + function saveNewSection(e) { e.preventDefault(); @@ -430,6 +478,48 @@ function cancelNewSection(e) { $(this).parents('section.new-section').remove(); } + +function addNewCourse(e) { + e.preventDefault(); + var $newCourse = $($('#new-course-template').html()); + $('.new-course-button').after($newCourse); + $newCourse.find('.new-course-org').focus().select(); + $newCourse.find('.new-course-save').bind('click', saveNewCourse); + $newCourse.find('.new-course-cancel').bind('click', cancelNewCourse); +} + +function saveNewCourse(e) { + e.preventDefault(); + + template = $(this).data('template'); + + org = $(this).prevAll('.new-course-org').val(); + number = $(this).prevAll('.new-course-number').val(); + display_name = $(this).prevAll('.new-course-name').val(); + + if (org == '' || number == '' || display_name == ''){ + alert('You must specify all fields in order to create a new course.') + } + + $.post('/create_new_course', + { 'template' : template, + 'org' : org, + 'number' : number, + 'display_name': display_name, + }, + function(data) { + if (data.id != undefined) + location.reload(); + else if (data.ErrMsg != undefined) + alert(data.ErrMsg); + }); +} + +function cancelNewCourse(e) { + e.preventDefault(); + $(this).parents('section.new-course').remove(); +} + function addNewSubsection(e) { e.preventDefault(); var $section = $(this).closest('.courseware-section'); diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 69b51b727e..c1875edb06 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -217,6 +217,12 @@ code { } } +body.hide-wip { + .wip, .wip-box { + display: none !important; + } +} + body.show-wip { .wip { outline: 1px solid #f00 !important; diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 5eeefaecaa..0b893e1965 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -310,7 +310,7 @@ } } - .preview-button { + .preview-button, .view-button { @include white-button; margin-bottom: 10px; } @@ -325,7 +325,8 @@ .save-button, .preview-button, - .publish-button { + .publish-button, + .view-button { font-size: 11px; margin-top: 10px; padding: 6px 15px 8px; @@ -427,17 +428,15 @@ } .edit-state-draft { - .visibility { + .visibility, + .edit-draft-message, + .view-button { display: none; } .published-alert { display: block; } - - .edit-draft-message { - display: none; - } } .edit-state-public { @@ -446,7 +445,8 @@ .component-actions, .new-component-item, #published-alert, - .publish-draft-message { + .publish-draft-message, + .preview-button { display: none; } @@ -463,7 +463,8 @@ #delete-draft, #publish-draft, #published-alert, - #create-draft, { + #create-draft, + .view-button { display: none; } } diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 5940767c86..1b3ffb6d5d 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -10,7 +10,7 @@

Asset Library

Upload New File - +
@@ -26,7 +26,11 @@ % for asset in assets:
-
+
+ % if asset['thumb_url'] is not None: + + % endif +
${asset['displayname']} diff --git a/cms/templates/base.html b/cms/templates/base.html index f839cb9753..ba91b2f400 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -17,7 +17,7 @@ <%block name="header_extras"> - + <%include file="widgets/header.html" args="active_tab=active_tab"/> <%include file="courseware_vendor_js.html"/> @@ -32,7 +32,7 @@ - + diff --git a/cms/templates/import.html b/cms/templates/import.html index 7ef7bcb79a..42def2d512 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -1,4 +1,6 @@ <%inherit file="base.html" /> +<%namespace name='static' file='static_content.html'/> + <%! from django.core.urlresolvers import reverse %> <%block name="title">Import <%block name="bodyclass">import @@ -21,6 +23,10 @@

change

+ @@ -28,7 +34,6 @@ <%block name="jsextra"> - + +<%block name="content">

My Courses

- New Course + New Course
    %for course, url in courses:
  • diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 142afc2304..3adfa42a16 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -12,7 +12,7 @@

    The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional source staff below. Please note that they must have already registered and verified their account.

diff --git a/cms/templates/overview.html b/cms/templates/overview.html index eb114b785f..9739f4c1a2 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -46,7 +46,7 @@

Courseware

-
+
- +
@@ -67,7 +67,7 @@ New Subsection
-
    +
      % for subsection in section.get_children():
${units.enum_units(subsection)} diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 1348072d4c..0f4f999599 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -85,12 +85,13 @@

This is a draft of the published unit. To update the live version, you must replace it with this draft.

-

This unit is scheduled to be released to students on ${subsection.start} with the subsection "${subsection.display_name}"

+

This unit is scheduled to be released to students on ${release_date} with the subsection "${subsection.display_name}"

diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 08e998381a..6e56c4f591 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -10,10 +10,10 @@ ${context_course.display_name} % endif diff --git a/cms/urls.py b/cms/urls.py index 1c2e70b35d..d9f75d159e 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -16,8 +16,7 @@ urlpatterns = ('', url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'), url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'), url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'), - - + url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.course_index', name='course_index'), diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index e7e3e4e519..4883677bf4 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -16,6 +16,8 @@ log = logging.getLogger(__name__) class CourseDescriptor(SequenceDescriptor): module_class = SequenceModule + template_dir_name = 'course' + class Textbook: def __init__(self, title, book_url): self.title = title @@ -64,7 +66,6 @@ class CourseDescriptor(SequenceDescriptor): def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) - self.textbooks = [] for title, book_url in self.definition['data']['textbooks']: try: diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py index 41b1523709..dcb731b135 100644 --- a/common/lib/xmodule/xmodule/templates.py +++ b/common/lib/xmodule/xmodule/templates.py @@ -31,6 +31,8 @@ def all_templates(): templates = defaultdict(list) for category, descriptor in XModuleDescriptor.load_classes(): + if category == 'course': + logging.debug(descriptor.templates()) templates[category] = descriptor.templates() return templates @@ -65,8 +67,9 @@ def update_templates(): template_location = Location('i4x', 'edx', 'templates', category, Location.clean_for_url_name(template.metadata['display_name'])) try: - json_data = template._asdict() + json_data = {'definition': {'data': template.data, 'children' : template.children}} json_data['location'] = template_location.dict() + XModuleDescriptor.load_from_json(json_data, TemplateTestSystem()) except: log.warning('Unable to instantiate {cat} from template {template}, skipping'.format( diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml new file mode 100644 index 0000000000..b0af998ddd --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml @@ -0,0 +1,5 @@ +--- +metadata: + display_name: Empty +data: { 'textbooks' : [ ], 'wiki_slug' : null } +children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml index cc2502a855..1ae05e04b3 100644 --- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml @@ -8,7 +8,7 @@ data: |

A multiple choice response problem presents radio buttons for student - input. One or more of the choice may be correct.--> Correctness of + input. Correctness of input is evaluated based on expected answers specified within each "choice" stanza.

diff --git a/common/static/js/vendor/jquery.form.js b/common/static/js/vendor/jquery.form.js new file mode 100644 index 0000000000..d759914fea --- /dev/null +++ b/common/static/js/vendor/jquery.form.js @@ -0,0 +1,1117 @@ +/*! + * jQuery Form Plugin + * version: 3.18 (28-SEP-2012) + * @requires jQuery v1.5 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Project repository: https://github.com/malsup/form + * Dual licensed under the MIT and GPL licenses: + * http://malsup.github.com/mit-license.txt + * http://malsup.github.com/gpl-license-v2.txt + */ +/*global ActiveXObject alert */ +;(function($) { +"use strict"; + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are mutually exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').on('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the + form does not have to exist when you invoke ajaxForm: + + $('#myForm').ajaxForm({ + delegation: true, + target: '#output' + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * Feature detection + */ +var feature = {}; +feature.fileapi = $("").get(0).files !== undefined; +feature.formdata = window.FormData !== undefined; + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + /*jshint scripturl:true */ + + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + var method, action, url, $form = this; + + if (typeof options == 'function') { + options = { success: options }; + } + + method = this.attr('method'); + action = this.attr('action'); + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + + options = $.extend(true, { + url: url, + success: $.ajaxSettings.success, + type: method || 'GET', + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var traditional = options.traditional; + if ( traditional === undefined ) { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements); + if (options.data) { + options.extraData = options.data; + qx = $.param(options.data, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a, traditional); + if (qx) { + q = ( q ? (q + '&' + qx) : qx ); + } + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + if (options.resetForm) { + callbacks.push(function() { $form.resetForm(); }); + } + if (options.clearForm) { + callbacks.push(function() { $form.clearForm(options.includeHidden); }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + var fn = options.replaceTarget ? 'replaceWith' : 'html'; + $(options.target)[fn](data).each(oldSuccess, arguments); + }); + } + else if (options.success) { + callbacks.push(options.success); + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this ; // jQuery 1.4+ supports scope context + for (var i=0, max=callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + // are there files to upload? + var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113) + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + var fileAPI = feature.fileapi && feature.formdata; + log("fileAPI :" + fileAPI); + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + + var jqxhr; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + jqxhr = fileUploadIframe(a); + }); + } + else { + jqxhr = fileUploadIframe(a); + } + } + else if ((hasFileInputs || multipart) && fileAPI) { + jqxhr = fileUploadXhr(a); + } + else { + jqxhr = $.ajax(options); + } + + $form.removeData('jqxhr').data('jqxhr', jqxhr); + + // clear element array + for (var k=0; k < elements.length; k++) + elements[k] = null; + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + // utility fn for deep serialization + function deepSerialize(extraData){ + var serialized = $.param(extraData).split('&'); + var len = serialized.length; + var result = {}; + var i, part; + for (i=0; i < len; i++) { + part = serialized[i].split('='); + result[decodeURIComponent(part[0])] = decodeURIComponent(part[1]); + } + return result; + } + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i=0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + var serializedData = deepSerialize(options.extraData); + for (var p in serializedData) + if (serializedData.hasOwnProperty(p)) + formdata.append(p, serializedData[p]); + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType: false, + processData: false, + cache: false, + type: method || 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = jQuery.ajaxSettings.xhr(); + if (xhr.upload) { + xhr.upload.onprogress = function(event) { + var percent = 0; + var position = event.loaded || event.position; /*event.position is deprecated*/ + var total = event.total; + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + options.uploadProgress(event, position, total, percent); + }; + } + return xhr; + }; + } + + s.data = null; + var beforeSend = s.beforeSend; + s.beforeSend = function(xhr, o) { + o.data = formdata; + if(beforeSend) + beforeSend.call(this, xhr, o); + }; + return $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var useProp = !!$.fn.prop; + var deferred = $.Deferred(); + + if ($(':input[name=submit],:input[id=submit]', form).length) { + // if there is an input with a name or id of 'submit' then we won't be + // able to invoke the submit fn on the form (at least not x-browser) + alert('Error: Form elements must not have name or id of "submit".'); + deferred.reject(); + return deferred; + } + + if (a) { + // ensure that every serialized input is still enabled + for (i=0; i < elements.length; i++) { + el = $(elements[i]); + if ( useProp ) + el.prop('disabled', false); + else + el.removeAttr('disabled'); + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + (new Date().getTime()); + if (s.iframeTarget) { + $io = $(s.iframeTarget); + n = $io.attr('name'); + if (!n) + $io.attr('name', id); + else + id = n; + } + else { + $io = $('