Skip to content
This repository
Browse code

ENHANCEMENT Attaching files from /assets through UploadField

  • Loading branch information...
commit c00f0406e9becb3b4004ab3dc4616712812b1d7a 1 parent 55ddbd3
Ingo Schommer authored February 08, 2012
131  forms/UploadField.php
@@ -30,7 +30,9 @@ class UploadField extends FileField {
30 30
 	 */
31 31
 	public static $allowed_actions = array(
32 32
 		'upload',
33  
-		'handleItem'
  33
+		'attach',
  34
+		'handleItem',
  35
+		'handleSelect',
34 36
 	);
35 37
 
36 38
 	/**
@@ -38,6 +40,7 @@ class UploadField extends FileField {
38 40
 	 */
39 41
 	public static $url_handlers = array(
40 42
 		'item/$ID' => 'handleItem',
  43
+		'select' => 'handleSelect',
41 44
 		'$Action!' => '$Action',
42 45
 	);
43 46
 
@@ -293,6 +296,13 @@ protected function getThumbnailURLForFile(File $file) {
293 296
 		return false;
294 297
 	}
295 298
 
  299
+	public function getAttributes() {
  300
+		return array_merge(
  301
+			parent::getAttributes(),
  302
+			array('data-selectdialog-url', $this->Link('select'))
  303
+		);
  304
+	}
  305
+
296 306
 	public function Field() {
297 307
 		$record = $this->getRecord();
298 308
 		$name = $this->getName();
@@ -329,6 +339,8 @@ public function Field() {
329 339
 
330 340
 		$config = array(
331 341
 			'url' => $this->Link('upload'),
  342
+			'urlSelectDialog' => $this->Link('select'),
  343
+			'urlAttach' => $this->Link('attach'),
332 344
 			'acceptFileTypes' => '.+$',
333 345
 			'maxNumberOfFiles' => $this->getConfig('allowedMaxFileNumber')
334 346
 		);
@@ -393,6 +405,14 @@ public function getItemHandler($itemID) {
393 405
 	}
394 406
 
395 407
 	/**
  408
+	 * @param SS_HTTPRequest $request
  409
+	 * @return UploadField_ItemHandler
  410
+	 */
  411
+	public function handleSelect(SS_HTTPRequest $request) {
  412
+		return Object::create('UploadField_SelectHandler', $this, $this->folderName);
  413
+	}
  414
+
  415
+	/**
396 416
 	 * Action to handle upload of a single file
397 417
 	 * 
398 418
 	 * @param SS_HTTPRequest $request
@@ -468,6 +488,34 @@ public function upload(SS_HTTPRequest $request) {
468 488
 	}
469 489
 
470 490
 	/**
  491
+	 * Add existing {@link File} records to the relationship.
  492
+	 */
  493
+	public function attach($request) {
  494
+		if(!$request->isPOST()) return $this->httpError(403);
  495
+		if(!$this->managesRelation()) return $this->httpError(403);
  496
+
  497
+		$return = array();
  498
+
  499
+		$files = DataList::create('File')->byIDs($request->postVar('ids'));
  500
+		foreach($files as $file) {
  501
+			$this->attachFile($file);
  502
+			$file =  $this->customiseFile($file);
  503
+			$return[] = array(
  504
+				'id' => $file->ID,
  505
+				'name' => $file->getTitle() . '.' . $file->getExtension(),
  506
+				'url' => $file->getURL(),
  507
+				'thumbnail_url' => $file->UploadFieldThumbnailURL,
  508
+				'edit_url' => $file->UploadFieldEditLink,
  509
+				'size' => $file->getAbsoluteSize(),
  510
+				'buttons' => $file->UploadFieldFileButtons
  511
+			);
  512
+		}
  513
+		$response = new SS_HTTPResponse(Convert::raw2json($return));
  514
+		$response->addHeader('Content-Type', 'application/json');
  515
+		return $response;
  516
+	}
  517
+
  518
+	/**
471 519
 	 * @param File
472 520
 	 */
473 521
 	protected function attachFile($file) {
@@ -742,3 +790,84 @@ public function doEdit(array $data, Form $form, SS_HTTPRequest $request) {
742 790
 	
743 791
 }
744 792
 
  793
+
  794
+class UploadField_SelectHandler extends RequestHandler {
  795
+
  796
+	/**
  797
+	 * @var UploadField
  798
+	 */
  799
+	protected $parent;
  800
+
  801
+	/**
  802
+	 * @var String
  803
+	 */
  804
+	protected $folderName;
  805
+
  806
+	public static $url_handlers = array(
  807
+		'$Action!' => '$Action',
  808
+		'' => 'index',
  809
+	);
  810
+
  811
+	function __construct($parent, $folderName = null) {
  812
+		$this->parent = $parent;
  813
+		$this->folderName = $folderName;
  814
+
  815
+		parent::__construct();
  816
+	}
  817
+
  818
+	function index() {
  819
+		return $this->renderWith('CMSDialog');
  820
+	}
  821
+
  822
+	/**
  823
+	 * @param string $action
  824
+	 * @return string
  825
+	 */
  826
+	public function Link($action = null) {
  827
+		return Controller::join_links($this->parent->Link(), '/select/', $action);
  828
+	}
  829
+
  830
+	/**
  831
+	 * @return Form
  832
+	 */
  833
+	function Form() {
  834
+		$action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)'));
  835
+		$action->addExtraClass('ss-ui-action-constructive');
  836
+		return new Form(
  837
+			$this,
  838
+			'Form',
  839
+			new FieldList($this->getListField()),
  840
+			new FieldList($action)
  841
+		);
  842
+	}
  843
+
  844
+	/**
  845
+	 * @return FormField
  846
+	 */
  847
+	protected function getListField() {
  848
+		$folder = $this->getFolder();
  849
+		$config = GridFieldConfig::create();
  850
+		$config->addComponent(new GridFieldSortableHeader());
  851
+		$config->addComponent(new GridFieldFilter());
  852
+		$config->addComponent(new GridFieldDefaultColumns());
  853
+		$config->addComponent(new GridFieldPaginator(10));
  854
+
  855
+		$field = new GridField('Files', false, $folder->stageChildren(), $config);
  856
+		$field->setAttribute('data-selectable', true);
  857
+		if($this->parent->getConfig('allowedMaxFileNumber') > 1) $field->setAttribute('data-multiselect', true);
  858
+
  859
+		return $field;
  860
+	}
  861
+
  862
+	/**
  863
+	 * @return Folder
  864
+	 */
  865
+	function getFolder() {
  866
+		return Folder::find_or_make($this->folderName);
  867
+	}
  868
+
  869
+	function doAttach($data, $form) {
  870
+		// TODO Only implemented via JS for now
  871
+	}
  872
+
  873
+}
107  javascript/UploadField.js
... ...
@@ -1,12 +1,12 @@
1 1
 (function($) {
2 2
 	$.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
3 3
 		_initTemplates: function() {
4  
-	        this.options.templateContainer = document.createElement(
5  
-	            this._files.prop('nodeName')
6  
-	        );
7  
-	        this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName);
8  
-	        this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName);
9  
-	    },
  4
+					this.options.templateContainer = document.createElement(
  5
+							this._files.prop('nodeName')
  6
+					);
  7
+					this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName);
  8
+					this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName);
  9
+			},
10 10
 		_enableFileInputButton: function() {
11 11
 			$.blueimpUI.fileupload.prototype._enableFileInputButton.call(this);
12 12
 			this.element.find('.ss-uploadfield-addfile').show();
@@ -26,10 +26,15 @@
26 26
 	});
27 27
 	$.entwine('ss', function($) {
28 28
 		$('div.ss-upload').entwine({
  29
+
  30
+			Config: null,
  31
+
29 32
 			onmatch: function() {
30 33
 				var fileInput = this.find('input');
31 34
 				var dropZone = this.find('.ss-uploadfield-dropzone');
32 35
 				var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"'));
  36
+
  37
+				this.setConfig(config);
33 38
 				this.fileupload($.extend(true, 
34 39
 					{
35 40
 						formData: function(form) {
@@ -52,22 +57,22 @@
52 57
 							emptyResult: ss.i18n._t('UploadField.EMPTYRESULT')
53 58
 						},
54 59
 						send: function(e, data) {
55  
-						    if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') {
56  
-						        // Iframe Transport does not support progress events.
57  
-						        // In lack of an indeterminate progress bar, we set
58  
-						        // the progress to 100%, showing the full animated bar:
59  
-						        data.total = 1;
60  
-						        data.loaded = 1;
61  
-						        $(this).data('fileupload').options.progress(e, data);
62  
-						    }
  60
+								if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') {
  61
+										// Iframe Transport does not support progress events.
  62
+										// In lack of an indeterminate progress bar, we set
  63
+										// the progress to 100%, showing the full animated bar:
  64
+										data.total = 1;
  65
+										data.loaded = 1;
  66
+										$(this).data('fileupload').options.progress(e, data);
  67
+								}
63 68
 						},
64 69
 						progress: function(e, data) {
65  
-					        if (data.context) {
66  
-					        	var value = parseInt(data.loaded / data.total * 100, 10) + '%';
67  
-					        	data.context.find('.ss-uploadfield-item-status').html((data.total == 1)?ss.i18n._t('UploadField.LOADING'):value);
68  
-					            data.context.find('.ss-uploadfield-item-progressbarvalue').css('width', value);
69  
-					        }
70  
-					    }
  70
+									if (data.context) {
  71
+										var value = parseInt(data.loaded / data.total * 100, 10) + '%';
  72
+										data.context.find('.ss-uploadfield-item-status').html((data.total == 1)?ss.i18n._t('UploadField.LOADING'):value);
  73
+											data.context.find('.ss-uploadfield-item-progressbarvalue').css('width', value);
  74
+									}
  75
+							}
71 76
 					}, 
72 77
 					config, 
73 78
 					{
@@ -82,6 +87,62 @@
82 87
 					dropZone.show(); // drag&drop avaliable
83 88
 				}
84 89
 				this._super();
  90
+			},
  91
+
  92
+			openSelectDialog: function() {
  93
+				// Create dialog and load iframe
  94
+				var self = this, config = this.getConfig(), dialogId = 'ss-uploadfield-dialog-' + this.attr('id'), dialog = jQuery('#' + dialogId);
  95
+				if(!dialog.length) dialog = jQuery('<div class="ss-uploadfield-dialog" id="' + dialogId + '" />');
  96
+
  97
+				// Show dialog
  98
+				dialog.ssdialog({iframeUrl: config['urlSelectDialog']});
  99
+
  100
+				// TODO Allow single-select
  101
+				dialog.find('iframe').bind('load', function(e) {
  102
+					var contents = $(this).contents(), gridField = contents.find('fieldset.ss-gridfield');
  103
+					// TODO Fix jQuery custom event bubbling across iframes on same domain
  104
+					// gridField.find('.ss-gridfield-items')).bind('selectablestop', function() {
  105
+					// });
  106
+
  107
+					// Remove top margin (easier than including new selectors)
  108
+					contents.find('table.ss-gridfield').css('margin-top', 0);
  109
+
  110
+					// Can't use live() in iframes...
  111
+					contents.find('input[name=action_doAttach]').unbind('click.openSelectDialog').bind('click.openSelectDialog', function() {
  112
+						// TODO Fix entwine method calls across iframe/document boundaries
  113
+						var ids = $.map(gridField.find('.ss-gridfield-item.ui-selected'), function(el) {return $(el).data('id');});
  114
+						if(ids && ids.length) self.attachFiles(ids);
  115
+
  116
+						dialog.ssdialog('close');
  117
+						return false;
  118
+					});
  119
+				});
  120
+				dialog.ssdialog('open');
  121
+			},
  122
+			attachFiles: function(ids) {
  123
+				var self = this, config = this.getConfig();
  124
+				$.post(
  125
+					config['urlAttach'], 
  126
+					{'ids': ids},
  127
+					function(data, status, xhr) {
  128
+						var fn = self.fileupload('option', 'downloadTemplate');
  129
+						self.find('.ss-uploadfield-files').append(fn({
  130
+							files: data,
  131
+							formatFileSize: function (bytes) {
  132
+								if (typeof bytes !== 'number') return '';
  133
+								if (bytes >= 1000000000) return (bytes / 1000000000).toFixed(2) + ' GB';
  134
+								if (bytes >= 1000000) return (bytes / 1000000).toFixed(2) + ' MB';
  135
+								return (bytes / 1000).toFixed(2) + ' KB';
  136
+							},
  137
+							options: self.fileupload('option')
  138
+						}));
  139
+					}
  140
+				);
  141
+			}
  142
+		});
  143
+		$('div.ss-upload *').entwine({
  144
+			getUploadField: function() {
  145
+				return this.parents('div.ss-upload:first');
85 146
 			}
86 147
 		});
87 148
 		$('div.ss-upload .ss-uploadfield-files .ss-uploadfield-item').entwine({
@@ -160,5 +221,11 @@
160 221
 				});
161 222
 			}
162 223
 		});
  224
+		$('div.ss-upload .ss-uploadfield-fromfiles').entwine({
  225
+			onclick: function(e) {
  226
+				e.preventDefault();
  227
+				this.getUploadField().openSelectDialog();
  228
+			}
  229
+		});
163 230
 	});
164 231
 }(jQuery));
88  tests/forms/uploadfield/UploadFieldTest.php
@@ -375,6 +375,84 @@ function testDisabled() {
375 375
 		
376 376
 	}
377 377
 
  378
+	function testSelect() {
  379
+		$this->loginWithPermission('ADMIN');
  380
+
  381
+		$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
  382
+		$file4 = $this->objFromFixture('File', 'file4');
  383
+		$file5 = $this->objFromFixture('File', 'file5');
  384
+		$fileSubfolder = $this->objFromFixture('File', 'file-subfolder');
  385
+		$fileNoEdit = $this->objFromFixture('File', 'file-noedit');
  386
+
  387
+		$response = $this->get('UploadFieldTest_Controller/Form/field/ManyManyFiles/select/');
  388
+		$this->assertFalse($response->isError());
  389
+
  390
+		// A bit too much coupling with GridField, but a full template overload would make things too complex
  391
+		$parser = new CSSContentParser($response->getBody());
  392
+		$items = $parser->getBySelector('.ss-gridfield-item');
  393
+		$itemIDs = array_map(create_function('$el', 'return (int)$el["data-id"];'), $items);
  394
+		$this->assertContains($file4->ID, $itemIDs, 'Contains file in assigned folder');
  395
+		$this->assertNotContains($fileSubfolder->ID, $itemIDs, 'Does not contain file in subfolder');
  396
+	}
  397
+
  398
+	function testAttachHasOne() {
  399
+		$this->loginWithPermission('ADMIN');
  400
+
  401
+		$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
  402
+		$file1 = $this->objFromFixture('File', 'file1');
  403
+		$file2 = $this->objFromFixture('File', 'file2');
  404
+		$file3AlreadyAttached = $this->objFromFixture('File', 'file3');
  405
+
  406
+		$response = $this->post(
  407
+			'UploadFieldTest_Controller/Form/field/HasOneFile/attach', 
  408
+			array('ids' => array($file1->ID/* first file should be ignored */, $file2->ID))
  409
+		);
  410
+		$this->assertFalse($response->isError());
  411
+
  412
+		$record = DataObject::get_by_id($record->class, $record->ID, false);
  413
+		$this->assertEquals($file2->ID, $record->HasOneFileID, 'Attaches new relations');
  414
+	}
  415
+
  416
+	function testAttachHasMany() {
  417
+		$this->loginWithPermission('ADMIN');
  418
+
  419
+		$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
  420
+		$file1 = $this->objFromFixture('File', 'file1');
  421
+		$file2 = $this->objFromFixture('File', 'file2');
  422
+		$file3AlreadyAttached = $this->objFromFixture('File', 'file3');
  423
+
  424
+		$response = $this->post(
  425
+			'UploadFieldTest_Controller/Form/field/HasManyFiles/attach', 
  426
+			array('ids' => array($file1->ID, $file2->ID))
  427
+		);
  428
+		$this->assertFalse($response->isError());
  429
+
  430
+		$record = DataObject::get_by_id($record->class, $record->ID, false);
  431
+		$this->assertContains($file1->ID, $record->HasManyFiles()->column('ID'), 'Attaches new relations');
  432
+		$this->assertContains($file2->ID, $record->HasManyFiles()->column('ID'), 'Attaches new relations');
  433
+		$this->assertContains($file3AlreadyAttached->ID, $record->HasManyFiles()->column('ID'), 'Does not detach existing relations');
  434
+	}
  435
+
  436
+	function testAttachManyMany() {
  437
+		$this->loginWithPermission('ADMIN');
  438
+
  439
+		$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
  440
+		$file1 = $this->objFromFixture('File', 'file1');
  441
+		$file2 = $this->objFromFixture('File', 'file2');
  442
+		$file5AlreadyAttached = $this->objFromFixture('File', 'file5');
  443
+
  444
+		$response = $this->post(
  445
+			'UploadFieldTest_Controller/Form/field/ManyManyFiles/attach', 
  446
+			array('ids' => array($file1->ID, $file2->ID))
  447
+		);
  448
+		$this->assertFalse($response->isError());
  449
+
  450
+		$record = DataObject::get_by_id($record->class, $record->ID, false);
  451
+		$this->assertContains($file1->ID, $record->ManyManyFiles()->column('ID'), 'Attaches new relations');
  452
+		$this->assertContains($file2->ID, $record->ManyManyFiles()->column('ID'), 'Attaches new relations');
  453
+		$this->assertContains($file5AlreadyAttached->ID, $record->ManyManyFiles()->column('ID'), 'Does not detach existing relations');
  454
+	}
  455
+
378 456
 	protected function getMockForm() {
379 457
 		return new Form(new Controller(), 'Form', new FieldList(), new FieldList());
380 458
 	}
@@ -518,6 +596,10 @@ function Form() {
518 596
 		$fieldDisabled->setRecord($record);
519 597
 		$fieldDisabled = $fieldDisabled->performDisabledTransformation();
520 598
 
  599
+		$fieldSubfolder = new UploadField('SubfolderField');
  600
+		$fieldSubfolder->setFolderName('UploadFieldTest/subfolder1');
  601
+		$fieldSubfolder->setRecord($record);
  602
+
521 603
 		$form = new Form(
522 604
 			$this,
523 605
 			'Form',
@@ -527,7 +609,8 @@ function Form() {
527 609
 				$fieldHasMany,
528 610
 				$fieldManyMany,
529 611
 				$fieldReadonly,
530  
-				$fieldDisabled
  612
+				$fieldDisabled,
  613
+				$fieldSubfolder
531 614
 			),
532 615
 			new FieldList(
533 616
 				new FormAction('submit')
@@ -538,7 +621,8 @@ function Form() {
538 621
 				'HasManyFiles',
539 622
 				'ManyManyFiles',
540 623
 				'ReadonlyField',
541  
-				'DisabledField'
  624
+				'DisabledField',
  625
+				'SubfolderField'
542 626
 			)
543 627
 		);
544 628
 		return $form;
8  tests/forms/uploadfield/UploadFieldTest.yml
... ...
@@ -1,6 +1,9 @@
1 1
 Folder:
2 2
    folder1:
3 3
       Name: UploadFieldTest
  4
+   folder1-subfolder1:
  5
+   		Name: subfolder1
  6
+   		ParentID: =>Folder.folder1
4 7
 File:
5 8
 	file1:
6 9
 		Title: File1
@@ -37,6 +40,11 @@ File:
37 40
 		Name: nodelete.txt
38 41
 		Filename: assets/UploadFieldTest/nodelete.txt
39 42
 		ParentID: =>Folder.folder1
  43
+	file-subfolder:
  44
+		Title: file-subfolder.txt
  45
+		Name: file-subfolder.txt
  46
+		Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
  47
+		ParentID: =>Folder.folder1-subfolder1
40 48
 UploadFieldTest_Record:
41 49
 	record1:
42 50
 		Title: Record 1

0 notes on commit c00f040

Please sign in to comment.
Something went wrong with that request. Please try again.