diff --git a/css/redactor.css b/css/redactor.css
index e6d048dba9eec977e538f1ecd8f5992d8e02539d..052eec5b37c1de720864bbc5561917c0089ca744 100644
--- a/css/redactor.css
+++ b/css/redactor.css
@@ -153,6 +153,7 @@ body .redactor-box-fullscreen {
   background: #fff;
   border: none;
   box-shadow: 0 1px 4px -2px rgba(0, 0, 0, 0.4);
+  z-index: 1 !important;
 }
 .redactor-toolbar:after {
   content: "";
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index 37f6399ff80943bfc8056855c61d797cf813fd7b..1d4dc14236ad922978c8cefd7e8020bc1a6d14da 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -208,7 +208,7 @@ class DynamicFormsAjaxAPI extends AjaxController {
                 'value' => $I->getValue(),
                 'display' => $display,
                 'id' => $I->id,
-                'list_id' => $I->getList()->getId(),
+                'list_id' => $list->getId(),
             );
         }
         return $this->encode($results);
@@ -260,15 +260,16 @@ class DynamicFormsAjaxAPI extends AjaxController {
             'title' => sprintf('%s — %s',
                 $list->getName(), __('Import Items')),
             'action' => "#list/{$list_id}/import",
-            'upload_url' => "lists.php?id={$list_id}&do=import-users",
+            'upload_url' => "lists.php?id={$list_id}&do=import-items",
         );
 
         if ($_POST) {
-            $status = $list->importFromPost($_POST['pasted']);
-            if (is_string($status))
-                $info['error'] = $status;
-            else
-                Http::response(201, $this->encode(array('success' => true, 'count' => $status)));
+            $status = $list->importFromPost($_FILES['import'] ?: $_POST['pasted']);
+            if ($status && is_numeric($status))
+                Http::response(201, $this->encode( array('success' => true, 'count' => $status)));
+
+            $info['error'] = $status;
+            $info['pasted'] = Format::htmlchars($_POST['pasted']);
         }
 
         include(STAFFINC_DIR . 'templates/list-import.tmpl.php');
diff --git a/include/class.import.php b/include/class.import.php
index c128136b7439845714435a503f89ed131609e0bd..dc9a027c1d4af5606a6f9f2df293f9e097d5b72f 100644
--- a/include/class.import.php
+++ b/include/class.import.php
@@ -84,7 +84,8 @@ class CsvImporter {
                     break;
                 }
                 else {
-                    throw new ImportError(sprintf(__('%s: Unable to map header to a user field'), $h));
+                    throw new ImportError(sprintf(
+                                __('%s: Unable to map header to the object field'), $h));
                 }
             }
         }
diff --git a/include/class.list.php b/include/class.list.php
index bd6ed087eff317ab6cc26507f626f4ef7b24fea7..f2b1b1301c028356dfe5db0050676520358c70a6 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -523,11 +523,11 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $selections;
     }
 
-    function importCsv($stream, $defaults=array()) {
-        //Read the header (if any)
-        $headers = array('value' => __('Value'), 'abbrev' => __('Abbreviation'));
+   function importCsv($stream, $defaults=array()) {
+        require_once INCLUDE_DIR . 'class.import.php';
+
         $form = $this->getConfigurationForm();
-        $named_fields = $fields = array(
+        $fields = array(
             'value' => new TextboxField(array(
                 'label' => __('Value'),
                 'name' => 'value',
@@ -536,119 +536,42 @@ class DynamicList extends VerySimpleModel implements CustomList {
                 ),
             )),
             'abbrev' => new TextboxField(array(
-                'name' => 'abbrev',
+                'name' => 'extra',
                 'label' => __('Abbreviation'),
                 'configuration' => array(
                     'length' => 0,
                 ),
             )),
         );
-        $all_fields = $form->getFields();
-        $has_header = false;
-        foreach ($all_fields as $f)
-            if ($f->get('name'))
-                $named_fields[] = $f;
-
-        if (!($data = fgetcsv($stream, 1000, ",")))
-            return __('Whoops. Perhaps you meant to send some CSV records');
-
-        foreach ($data as $D) {
-            if (strcasecmp($D, 'value') === 0)
-                $has_header = true;
-        }
-        if ($has_header) {
-            foreach ($data as $h) {
-                $found = false;
-                foreach ($all_fields as $f) {
-                    if (in_array(mb_strtolower($h), array(
-                            mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
-                        $found = true;
-                        if (!$f->get('name'))
-                            return sprintf(__(
-                                '%s: Field must have `variable` set to be imported'), $h);
-                        $headers[$f->get('name')] = $f->get('label');
-                        break;
-                    }
-                }
-                if (!$found) {
-                    $has_header = false;
-                    if (count($data) == count($named_fields)) {
-                        // Number of fields in the user form matches the number
-                        // of fields in the data. Assume things line up
-                        $headers = array();
-                        foreach ($named_fields as $f)
-                            $headers[$f->get('name')] = $f->get('label');
-                        break;
-                    }
-                    else {
-                        return sprintf(__('%s: Unable to map header to a property'), $h);
-                    }
-                }
-            }
-        }
-
-        // 'value' MUST be in the headers
-        if (!isset($headers['value']))
-            return __('CSV file must include `value` column');
-
-        if (!$has_header)
-            fseek($stream, 0);
 
-        $items = array();
-        $keys = array('value', 'abbrev');
-        foreach ($headers as $h => $label) {
-            if (!($f = $form->getField($h)))
-                continue;
-
-            $name = $keys[] = $f->get('name');
-            $fields[$name] = $f->getImpl();
-        }
-
-        // Add default fields (org_id, etc).
-        foreach ($defaults as $key => $val) {
-            // Don't apply defaults which are also being imported
-            if (isset($header[$key]))
-                unset($defaults[$key]);
-            $keys[] = $key;
+        $form = $this->getConfigurationForm();
+        if ($form && ($custom_fields = $form->getFields())
+                && count($custom_fields)) {
+            foreach ($custom_fields as $f)
+                if ($f->get('name'))
+                    $fields[$f->get('name')] = $f;
         }
 
-        while (($data = fgetcsv($stream, 1000, ",")) !== false) {
-            if (count($data) == 1 && $data[0] == null)
-                // Skip empty rows
-                continue;
-            elseif (count($data) != count($headers))
-                return sprintf(__('Bad data. Expected: %s'), implode(', ', $headers));
-            // Validate according to field configuration
-            $i = 0;
-            foreach ($headers as $h => $label) {
-                $f = $fields[$h];
-                $T = $f->parse($data[$i]);
-                if ($f->validateEntry($T) && $f->errors())
-                    return sprintf(__(
-                        /* 1 will be a field label, and 2 will be error messages */
-                        '%1$s: Invalid data: %2$s'),
-                        $label, implode(', ', $f->errors()));
-                // Convert to database format
-                $data[$i] = $f->to_database($T);
-                $i++;
+        $importer = new CsvImporter($stream);
+        $imported = 0;
+        try {
+            db_autocommit(false);
+            $records = $importer->importCsv($fields, $defaults);
+            foreach ($records as $data) {
+                $errors = array();
+                $item = $this->addItem($data, $errors);
+                if ($item && $item->setConfiguration($data, $errors))
+                    $imported++;
+                else
+                    echo sprintf(__('Unable to import item: %s'), print_r($data, true));
             }
-            // Add default fields
-            foreach ($defaults as $key => $val)
-                $data[] = $val;
-
-            $items[] = $data;
+            db_autocommit(true);
         }
-
-        foreach ($items as $u) {
-            $vars = array_combine($keys, $u);
-            $errors = array();
-            $item = $this->addItem($vars, $errors);
-            if (!$item || !$item->setConfiguration($vars, $errors))
-                return sprintf(__('Unable to import item: %s'),
-                    print_r($vars, true));
+        catch (Exception $ex) {
+            db_rollback();
+            return $ex->getMessage();
         }
-
-        return count($items);
+        return $imported;
     }
 
     function importFromPost($stuff, $extra=array()) {
diff --git a/scp/lists.php b/scp/lists.php
index c5a552f3e17dfbe78de5f09c6c089f967092e8f8..6ec0fdc9259b5710144f9dfa131f46293832ce44 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -25,7 +25,7 @@ if ($criteria) {
 $errors = array();
 
 if($_POST) {
-    switch(strtolower($_POST['do'])) {
+    switch(strtolower($_REQUEST['do'])) {
         case 'update':
             if (!$list)
                 $errors['err']=sprintf(__('%s: Unknown or invalid ID.'),