From c00f0406e9becb3b4004ab3dc4616712812b1d7a Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 8 Feb 2012 00:58:58 +0100 Subject: [PATCH] ENHANCEMENT Attaching files from /assets through UploadField --- forms/UploadField.php | 131 +++++++++++++++++++- javascript/UploadField.js | 107 +++++++++++++--- tests/forms/uploadfield/UploadFieldTest.php | 88 ++++++++++++- tests/forms/uploadfield/UploadFieldTest.yml | 8 ++ 4 files changed, 311 insertions(+), 23 deletions(-) diff --git a/forms/UploadField.php b/forms/UploadField.php index e10268206..3e80b2a78 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -30,7 +30,9 @@ class UploadField extends FileField { */ public static $allowed_actions = array( 'upload', - 'handleItem' + 'attach', + 'handleItem', + 'handleSelect', ); /** @@ -38,6 +40,7 @@ class UploadField extends FileField { */ public static $url_handlers = array( 'item/$ID' => 'handleItem', + 'select' => 'handleSelect', '$Action!' => '$Action', ); @@ -293,6 +296,13 @@ class UploadField extends FileField { return false; } + public function getAttributes() { + return array_merge( + parent::getAttributes(), + array('data-selectdialog-url', $this->Link('select')) + ); + } + public function Field() { $record = $this->getRecord(); $name = $this->getName(); @@ -329,6 +339,8 @@ class UploadField extends FileField { $config = array( 'url' => $this->Link('upload'), + 'urlSelectDialog' => $this->Link('select'), + 'urlAttach' => $this->Link('attach'), 'acceptFileTypes' => '.+$', 'maxNumberOfFiles' => $this->getConfig('allowedMaxFileNumber') ); @@ -392,6 +404,14 @@ class UploadField extends FileField { return Object::create('UploadField_ItemHandler', $this, $itemID); } + /** + * @param SS_HTTPRequest $request + * @return UploadField_ItemHandler + */ + public function handleSelect(SS_HTTPRequest $request) { + return Object::create('UploadField_SelectHandler', $this, $this->folderName); + } + /** * Action to handle upload of a single file * @@ -467,6 +487,34 @@ class UploadField extends FileField { return $response; } + /** + * Add existing {@link File} records to the relationship. + */ + public function attach($request) { + if(!$request->isPOST()) return $this->httpError(403); + if(!$this->managesRelation()) return $this->httpError(403); + + $return = array(); + + $files = DataList::create('File')->byIDs($request->postVar('ids')); + foreach($files as $file) { + $this->attachFile($file); + $file = $this->customiseFile($file); + $return[] = array( + 'id' => $file->ID, + 'name' => $file->getTitle() . '.' . $file->getExtension(), + 'url' => $file->getURL(), + 'thumbnail_url' => $file->UploadFieldThumbnailURL, + 'edit_url' => $file->UploadFieldEditLink, + 'size' => $file->getAbsoluteSize(), + 'buttons' => $file->UploadFieldFileButtons + ); + } + $response = new SS_HTTPResponse(Convert::raw2json($return)); + $response->addHeader('Content-Type', 'application/json'); + return $response; + } + /** * @param File */ @@ -742,3 +790,84 @@ class UploadField_ItemHandler extends RequestHandler { } + +class UploadField_SelectHandler extends RequestHandler { + + /** + * @var UploadField + */ + protected $parent; + + /** + * @var String + */ + protected $folderName; + + public static $url_handlers = array( + '$Action!' => '$Action', + '' => 'index', + ); + + function __construct($parent, $folderName = null) { + $this->parent = $parent; + $this->folderName = $folderName; + + parent::__construct(); + } + + function index() { + return $this->renderWith('CMSDialog'); + } + + /** + * @param string $action + * @return string + */ + public function Link($action = null) { + return Controller::join_links($this->parent->Link(), '/select/', $action); + } + + /** + * @return Form + */ + function Form() { + $action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)')); + $action->addExtraClass('ss-ui-action-constructive'); + return new Form( + $this, + 'Form', + new FieldList($this->getListField()), + new FieldList($action) + ); + } + + /** + * @return FormField + */ + protected function getListField() { + $folder = $this->getFolder(); + $config = GridFieldConfig::create(); + $config->addComponent(new GridFieldSortableHeader()); + $config->addComponent(new GridFieldFilter()); + $config->addComponent(new GridFieldDefaultColumns()); + $config->addComponent(new GridFieldPaginator(10)); + + $field = new GridField('Files', false, $folder->stageChildren(), $config); + $field->setAttribute('data-selectable', true); + if($this->parent->getConfig('allowedMaxFileNumber') > 1) $field->setAttribute('data-multiselect', true); + + return $field; + } + + /** + * @return Folder + */ + function getFolder() { + return Folder::find_or_make($this->folderName); + } + + function doAttach($data, $form) { + // TODO Only implemented via JS for now + } + +} \ No newline at end of file diff --git a/javascript/UploadField.js b/javascript/UploadField.js index b0f0be89f..1474ede3d 100644 --- a/javascript/UploadField.js +++ b/javascript/UploadField.js @@ -1,12 +1,12 @@ (function($) { $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, { _initTemplates: function() { - this.options.templateContainer = document.createElement( - this._files.prop('nodeName') - ); - this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName); - this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName); - }, + this.options.templateContainer = document.createElement( + this._files.prop('nodeName') + ); + this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName); + this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName); + }, _enableFileInputButton: function() { $.blueimpUI.fileupload.prototype._enableFileInputButton.call(this); this.element.find('.ss-uploadfield-addfile').show(); @@ -26,10 +26,15 @@ }); $.entwine('ss', function($) { $('div.ss-upload').entwine({ + + Config: null, + onmatch: function() { var fileInput = this.find('input'); var dropZone = this.find('.ss-uploadfield-dropzone'); var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"')); + + this.setConfig(config); this.fileupload($.extend(true, { formData: function(form) { @@ -52,22 +57,22 @@ emptyResult: ss.i18n._t('UploadField.EMPTYRESULT') }, send: function(e, data) { - if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') { - // Iframe Transport does not support progress events. - // In lack of an indeterminate progress bar, we set - // the progress to 100%, showing the full animated bar: - data.total = 1; - data.loaded = 1; - $(this).data('fileupload').options.progress(e, data); - } + if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.total = 1; + data.loaded = 1; + $(this).data('fileupload').options.progress(e, data); + } }, progress: function(e, data) { - if (data.context) { - var value = parseInt(data.loaded / data.total * 100, 10) + '%'; - data.context.find('.ss-uploadfield-item-status').html((data.total == 1)?ss.i18n._t('UploadField.LOADING'):value); - data.context.find('.ss-uploadfield-item-progressbarvalue').css('width', value); - } - } + if (data.context) { + var value = parseInt(data.loaded / data.total * 100, 10) + '%'; + data.context.find('.ss-uploadfield-item-status').html((data.total == 1)?ss.i18n._t('UploadField.LOADING'):value); + data.context.find('.ss-uploadfield-item-progressbarvalue').css('width', value); + } + } }, config, { @@ -82,6 +87,62 @@ dropZone.show(); // drag&drop avaliable } this._super(); + }, + + openSelectDialog: function() { + // Create dialog and load iframe + var self = this, config = this.getConfig(), dialogId = 'ss-uploadfield-dialog-' + this.attr('id'), dialog = jQuery('#' + dialogId); + if(!dialog.length) dialog = jQuery('
'); + + // Show dialog + dialog.ssdialog({iframeUrl: config['urlSelectDialog']}); + + // TODO Allow single-select + dialog.find('iframe').bind('load', function(e) { + var contents = $(this).contents(), gridField = contents.find('fieldset.ss-gridfield'); + // TODO Fix jQuery custom event bubbling across iframes on same domain + // gridField.find('.ss-gridfield-items')).bind('selectablestop', function() { + // }); + + // Remove top margin (easier than including new selectors) + contents.find('table.ss-gridfield').css('margin-top', 0); + + // Can't use live() in iframes... + contents.find('input[name=action_doAttach]').unbind('click.openSelectDialog').bind('click.openSelectDialog', function() { + // TODO Fix entwine method calls across iframe/document boundaries + var ids = $.map(gridField.find('.ss-gridfield-item.ui-selected'), function(el) {return $(el).data('id');}); + if(ids && ids.length) self.attachFiles(ids); + + dialog.ssdialog('close'); + return false; + }); + }); + dialog.ssdialog('open'); + }, + attachFiles: function(ids) { + var self = this, config = this.getConfig(); + $.post( + config['urlAttach'], + {'ids': ids}, + function(data, status, xhr) { + var fn = self.fileupload('option', 'downloadTemplate'); + self.find('.ss-uploadfield-files').append(fn({ + files: data, + formatFileSize: function (bytes) { + if (typeof bytes !== 'number') return ''; + if (bytes >= 1000000000) return (bytes / 1000000000).toFixed(2) + ' GB'; + if (bytes >= 1000000) return (bytes / 1000000).toFixed(2) + ' MB'; + return (bytes / 1000).toFixed(2) + ' KB'; + }, + options: self.fileupload('option') + })); + } + ); + } + }); + $('div.ss-upload *').entwine({ + getUploadField: function() { + return this.parents('div.ss-upload:first'); } }); $('div.ss-upload .ss-uploadfield-files .ss-uploadfield-item').entwine({ @@ -160,5 +221,11 @@ }); } }); + $('div.ss-upload .ss-uploadfield-fromfiles').entwine({ + onclick: function(e) { + e.preventDefault(); + this.getUploadField().openSelectDialog(); + } + }); }); }(jQuery)); \ No newline at end of file diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php index 62bc564ce..42d1aec5c 100644 --- a/tests/forms/uploadfield/UploadFieldTest.php +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -375,6 +375,84 @@ } + function testSelect() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file4 = $this->objFromFixture('File', 'file4'); + $file5 = $this->objFromFixture('File', 'file5'); + $fileSubfolder = $this->objFromFixture('File', 'file-subfolder'); + $fileNoEdit = $this->objFromFixture('File', 'file-noedit'); + + $response = $this->get('UploadFieldTest_Controller/Form/field/ManyManyFiles/select/'); + $this->assertFalse($response->isError()); + + // A bit too much coupling with GridField, but a full template overload would make things too complex + $parser = new CSSContentParser($response->getBody()); + $items = $parser->getBySelector('.ss-gridfield-item'); + $itemIDs = array_map(create_function('$el', 'return (int)$el["data-id"];'), $items); + $this->assertContains($file4->ID, $itemIDs, 'Contains file in assigned folder'); + $this->assertNotContains($fileSubfolder->ID, $itemIDs, 'Does not contain file in subfolder'); + } + + function testAttachHasOne() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file1 = $this->objFromFixture('File', 'file1'); + $file2 = $this->objFromFixture('File', 'file2'); + $file3AlreadyAttached = $this->objFromFixture('File', 'file3'); + + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasOneFile/attach', + array('ids' => array($file1->ID/* first file should be ignored */, $file2->ID)) + ); + $this->assertFalse($response->isError()); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertEquals($file2->ID, $record->HasOneFileID, 'Attaches new relations'); + } + + function testAttachHasMany() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file1 = $this->objFromFixture('File', 'file1'); + $file2 = $this->objFromFixture('File', 'file2'); + $file3AlreadyAttached = $this->objFromFixture('File', 'file3'); + + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasManyFiles/attach', + array('ids' => array($file1->ID, $file2->ID)) + ); + $this->assertFalse($response->isError()); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertContains($file1->ID, $record->HasManyFiles()->column('ID'), 'Attaches new relations'); + $this->assertContains($file2->ID, $record->HasManyFiles()->column('ID'), 'Attaches new relations'); + $this->assertContains($file3AlreadyAttached->ID, $record->HasManyFiles()->column('ID'), 'Does not detach existing relations'); + } + + function testAttachManyMany() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file1 = $this->objFromFixture('File', 'file1'); + $file2 = $this->objFromFixture('File', 'file2'); + $file5AlreadyAttached = $this->objFromFixture('File', 'file5'); + + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/attach', + array('ids' => array($file1->ID, $file2->ID)) + ); + $this->assertFalse($response->isError()); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertContains($file1->ID, $record->ManyManyFiles()->column('ID'), 'Attaches new relations'); + $this->assertContains($file2->ID, $record->ManyManyFiles()->column('ID'), 'Attaches new relations'); + $this->assertContains($file5AlreadyAttached->ID, $record->ManyManyFiles()->column('ID'), 'Does not detach existing relations'); + } + protected function getMockForm() { return new Form(new Controller(), 'Form', new FieldList(), new FieldList()); } @@ -518,6 +596,10 @@ class UploadFieldTest_Controller extends Controller implements TestOnly { $fieldDisabled->setRecord($record); $fieldDisabled = $fieldDisabled->performDisabledTransformation(); + $fieldSubfolder = new UploadField('SubfolderField'); + $fieldSubfolder->setFolderName('UploadFieldTest/subfolder1'); + $fieldSubfolder->setRecord($record); + $form = new Form( $this, 'Form', @@ -527,7 +609,8 @@ class UploadFieldTest_Controller extends Controller implements TestOnly { $fieldHasMany, $fieldManyMany, $fieldReadonly, - $fieldDisabled + $fieldDisabled, + $fieldSubfolder ), new FieldList( new FormAction('submit') @@ -538,7 +621,8 @@ class UploadFieldTest_Controller extends Controller implements TestOnly { 'HasManyFiles', 'ManyManyFiles', 'ReadonlyField', - 'DisabledField' + 'DisabledField', + 'SubfolderField' ) ); return $form; diff --git a/tests/forms/uploadfield/UploadFieldTest.yml b/tests/forms/uploadfield/UploadFieldTest.yml index 3453bcbb6..c9a7f14a4 100644 --- a/tests/forms/uploadfield/UploadFieldTest.yml +++ b/tests/forms/uploadfield/UploadFieldTest.yml @@ -1,6 +1,9 @@ Folder: folder1: Name: UploadFieldTest + folder1-subfolder1: + Name: subfolder1 + ParentID: =>Folder.folder1 File: file1: Title: File1 @@ -37,6 +40,11 @@ File: Name: nodelete.txt Filename: assets/UploadFieldTest/nodelete.txt ParentID: =>Folder.folder1 + file-subfolder: + Title: file-subfolder.txt + Name: file-subfolder.txt + Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt + ParentID: =>Folder.folder1-subfolder1 UploadFieldTest_Record: record1: Title: Record 1