API CHANGE Security admin supports adding, removing and searching for members by relations via gridfield

This contains some experimental API's when using GridFieldPopupForms on GridFieldPopupForms.

- GridFieldRelationAdd
- GridFieldRelationDelete
This commit is contained in:
Stig Lindqvist 2012-01-25 17:31:27 +13:00
parent 3f682531e6
commit c396c2d2ae
9 changed files with 458 additions and 36 deletions

14
admin/code/SecurityAdmin.php Normal file → Executable file
View File

@ -107,12 +107,14 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
* @return FieldList * @return FieldList
*/ */
function RootForm() { function RootForm() {
$memberList = new MemberTableField( $config = new GridFieldConfig();
$this, $config->addComponent(new GridFieldRelationAdd('Name'));
"Members" $config->addComponent(new GridFieldDefaultColumns());
); $config->addComponent(new GridFieldSortableHeader());
// unset 'inlineadd' permission, we don't want inline addition $config->addComponent(new GridFieldPaginator());
$memberList->setPermissions(array('edit', 'delete', 'add')); $config->addComponent(new GridFieldAction_Edit());
$config->addComponent(new GridFieldPopupForms($this, 'RootForm'));
$memberList = new GridField('Members', 'All members', DataList::create('Member'), $config);
$fields = new FieldList( $fields = new FieldList(
$root = new TabSet( $root = new TabSet(

View File

@ -511,7 +511,11 @@ class GridField extends FormField {
$actionName = $stateChange['actionName']; $actionName = $stateChange['actionName'];
$args = isset($stateChange['args']) ? $stateChange['args'] : array(); $args = isset($stateChange['args']) ? $stateChange['args'] : array();
$grid->handleAction($actionName, $args, $data); $html = $grid->handleAction($actionName, $args, $data);
if($html) {
return $html;
}
switch($request->getHeader('X-Get-Fragment')) { switch($request->getHeader('X-Get-Fragment')) {
case 'CurrentField': case 'CurrentField':
@ -539,13 +543,13 @@ class GridField extends FormField {
*/ */
public function handleAction($actionName, $args, $data) { public function handleAction($actionName, $args, $data) {
$actionName = strtolower($actionName); $actionName = strtolower($actionName);
foreach($this->components as $item) { foreach($this->components as $component) {
if(!($item instanceof GridField_ActionProvider)) { if(!($component instanceof GridField_ActionProvider)) {
continue; continue;
} }
if(in_array($actionName, array_map('strtolower', $item->getActions($this)))) { if(in_array($actionName, array_map('strtolower', $component->getActions($this)))) {
return $item->handleAction($this, $actionName, $args, $data); return $component->handleAction($this, $actionName, $args, $data);
} }
} }
throw new InvalidArgumentException("Can't handle action '$actionName'"); throw new InvalidArgumentException("Can't handle action '$actionName'");
@ -564,8 +568,6 @@ class GridField extends FormField {
$this->request = $request; $this->request = $request;
$this->setModel($model); $this->setModel($model);
///
foreach($this->components as $component) { foreach($this->components as $component) {
if(!($component instanceof GridField_URLHandler)) { if(!($component instanceof GridField_URLHandler)) {
continue; continue;

106
forms/gridfield/GridFieldPopupForms.php Normal file → Executable file
View File

@ -13,16 +13,51 @@ class GridFieldPopupForms implements GridField_URLHandler {
* @var String * @var String
*/ */
protected $template = 'GridFieldItemEditView'; protected $template = 'GridFieldItemEditView';
/**
*
* @var Controller
*/
protected $popupController;
/**
*
* @var string
*/
protected $popupFormName;
function getURLHandlers($gridField) { function getURLHandlers($gridField) {
return array( return array(
'item/$ID' => 'handleItem', 'item/$ID' => 'handleItem',
'autocomplete' => 'handleAutocomplete',
); );
} }
function handleItem($gridField, $request) { /**
* Create a popup component. The two arguments will specify how the popup form's HTML and
* behaviour is created. The given controller will be customised, putting the edit form into the
* template with the given name.
*
* The arguments are experimental API's to support partial content to be passed back to whatever
* controller who wants to display the getCMSFields
*
* @param Controller $popupController The controller object that will be used to render the pop-up forms
* @param string $popupFormName The name of the edit form to place into the pop-up form
*/
public function __construct($popupController, $popupFormName) {
$this->popupController = $popupController;
$this->popupFormName = $popupFormName;
}
/**
*
* @param type $gridField
* @param type $request
* @return GridFieldPopupForm_ItemRequest
*/
public function handleItem($gridField, $request) {
$record = $gridField->getList()->byId($request->param("ID")); $record = $gridField->getList()->byId($request->param("ID"));
$handler = new GridFieldPopupForm_ItemRequest($gridField, $this, $record); $handler = new GridFieldPopupForm_ItemRequest($gridField, $this, $record, $this->popupController, $this->popupFormName);
$handler->setTemplate($this->template); $handler->setTemplate($this->template);
return $handler; return $handler;
} }
@ -44,12 +79,36 @@ class GridFieldPopupForms implements GridField_URLHandler {
class GridFieldPopupForm_ItemRequest extends RequestHandler { class GridFieldPopupForm_ItemRequest extends RequestHandler {
/**
*
* @var GridField
*/
protected $gridField; protected $gridField;
/**
*
* @var GridField_URLHandler
*/
protected $component; protected $component;
/**
*
* @var DataObject
*/
protected $record; protected $record;
/**
*
* @var Controller
*/
protected $popupController;
/**
*
* @var string
*/
protected $popupFormName;
/** /**
* @var String * @var String
*/ */
@ -57,45 +116,66 @@ class GridFieldPopupForm_ItemRequest extends RequestHandler {
static $url_handlers = array( static $url_handlers = array(
'$Action!' => '$Action', '$Action!' => '$Action',
'' => 'index', '' => 'edit',
); );
function __construct($gridField, $component, $record) { /**
*
* @param GridFIeld $gridField
* @param GridField_URLHandler $component
* @param DataObject $record
* @param Controller $popupController
* @param string $popupFormName
*/
public function __construct($gridField, $component, $record, $popupController, $popupFormName) {
$this->gridField = $gridField; $this->gridField = $gridField;
$this->component = $gridField; $this->component = $gridField;
$this->record = $record; $this->record = $record;
$this->popupController = $popupController;
$this->popupFormName = $popupFormName;
parent::__construct(); parent::__construct();
} }
function Link($action = null) { public function Link($action = null) {
return Controller::join_links($this->gridField->Link('item'), $this->record->ID, $action); return Controller::join_links($this->gridField->Link('item'), $this->record->ID, $action);
} }
function edit($request) { function edit($request) {
$controller = $this->gridField->getForm()->Controller(); $controller = $this->popupController;
$return = $this->customise(array( $return = $this->customise(array(
'Backlink' => $controller->Link(), 'Backlink' => $this->gridField->getForm()->Controller()->Link(),
'ItemEditForm' => $this->ItemEditForm($this->gridField, $request), 'ItemEditForm' => $this->ItemEditForm($this->gridField, $request),
))->renderWith($this->template); ))->renderWith($this->template);
if($controller->isAjax()) { if($controller->isAjax()) {
return $return; return $return;
} else { } else {
// If not requested by ajax, we need to render it within the controller context+template // If not requested by ajax, we need to render it within the controller context+template
return $controller->customise(array( return $controller->customise(array(
$this->gridField->getForm()->Name() => $return, $this->popupFormName => $return,
)); ));
} }
} }
/**
* Builds an item edit form. The arguments to getCMSFields() are the popupController and
* popupFormName, however this is an experimental API and may change.
*
* In the future, we will probably need to come up with a tigher object representing a partially
* complete controller with gaps for extra functionality. This, for example, would be a better way
* of letting Security/login put its log-in form inside a UI specified elsewhere.
*
* @return Form
*/
function ItemEditForm() { function ItemEditForm() {
$request = $this->gridField->getForm()->Controller()->getRequest(); $request = $this->popupController->getRequest();
$form = new Form( $form = new Form(
$this, $this,
'ItemEditForm', 'ItemEditForm',
$this->record->getCMSFields(), // WARNING: The arguments passed here are a little arbitrary. This API will need cleanup
$this->record->getCMSFields($this->popupController, $this->popupFormName),
new FieldList( new FieldList(
$saveAction = new FormAction('doSave', _t('GridFieldDetailsForm.Save', 'Save')) $saveAction = new FormAction('doSave', _t('GridFieldDetailsForm.Save', 'Save'))
) )
@ -124,7 +204,7 @@ class GridFieldPopupForm_ItemRequest extends RequestHandler {
$form->sessionMessage($message, 'good'); $form->sessionMessage($message, 'good');
return $this->gridField->getForm()->Controller()->redirectBack(); return $this->popupController->redirectBack();
} }
/** /**

View File

@ -0,0 +1,198 @@
<?php
/**
* A GridFieldRelationAdd is responsible for adding objects to another objects
* has_many and many_many relation. It will not attach duplicate objects.
*
* It augments a GridField with fields above the gridfield to search and add
* objects to whatever the SS_List passed into the gridfield.
*
* If the object is set to use autosuggestion it will include jQuery UI
* autosuggestion field that searches for current objects that isn't already
* attached to the list.
*/
class GridFieldRelationAdd implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler {
/**
* Which template to use for rendering
*
* @var string $itemClass
*/
protected $itemClass = 'GridFieldRelationAdd';
/**
* Which column that should be used for doing a StartsWith search
*
* @var string
*/
protected $fieldToSearch = '';
/**
* Use the jQuery.ui.autosuggestion plugin
*
* @var bool
*/
protected $useAutoSuggestion = true;
/**
*
* @param string $fieldToSearch which field on the object in the list should be search
* @param bool $autoSuggestion - if you would like to use the javascript autosuggest feature
*/
public function __construct($fieldToSearch, $autoSuggestion=true) {
$this->fieldToSearch = $fieldToSearch;
$this->useAutoSuggestion = $autoSuggestion;
}
/**
*
* @param GridField $gridField
* @return string - HTML
*/
public function getHTMLFragments($gridField) {
$searchState = $gridField->State->GridFieldSearchRelation;
if($this->useAutoSuggestion){
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js');
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
Requirements::javascript(SAPPHIRE_DIR . "/javascript/GridFieldSearch.js");
}
$forTemplate = new ArrayData(array());
$forTemplate->Fields = new ArrayList();
$value = $this->findSingleEntry($gridField, $this->fieldToSearch, $searchState, $gridField->getList()->dataClass);
$searchField = new TextField('gridfield_relationsearch', 'Auto Suggest Search field', $value);
// Apparently the data-* needs to be double qouted for the jQuery.meta data plugin
//
$searchField->setAttribute('data-search-url', '\''.Controller::join_links($gridField->Link('search').'\''));
$findAction = new GridField_Action($gridField, 'gridfield_relationfind', 'Find', 'find', 'find');
$addAction = new GridField_Action($gridField, 'gridfield_relationadd', 'Add Relation', 'addto', 'addto');
// If an object is not found, disable the action
if(!is_int($gridField->State->GridFieldAddRelation)) {
$addAction->setReadonly(true);
}
$forTemplate->Fields->push($searchField);
$forTemplate->Fields->push($findAction);
$forTemplate->Fields->push($addAction);
return array('before' => $forTemplate->renderWith($this->itemClass));
}
/**
*
* @param GridField $gridField
* @return array
*/
public function getActions($gridField) {
return array('addto', 'find');
}
/**
* Manipulate the state to either add a new relation, or doing a small search
*
* @param GridField $gridField
* @param string $actionName
* @param string $arguments
* @param string $data
* @return string
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
switch($actionName) {
case 'addto':
if(isset($data['relationID']) && $data['relationID']){
$gridField->State->GridFieldAddRelation = $data['relationID'];
}
$gridField->State->GridFieldSearchRelation = '';
break;
case 'find' && isset($data['autosuggest_search']):
$gridField->State->GridFieldSearchRelation = $data['autosuggest_search'];
break;
}
}
/**
* If an object ID is set, add the object to the list
*
* @param GridField $gridField
* @param SS_List $dataList
* @return SS_List
*/
public function getManipulatedData(GridField $gridField, SS_List $dataList) {
if(!$gridField->State->GridFieldAddRelation) {
return $dataList;
}
$objectID = Convert::raw2sql($gridField->State->GridFieldAddRelation);
if($objectID) {
$object = DataObject::get_by_id($dataList->dataclass(), $objectID);
if($object) {
$dataList->add($object);
}
}
$gridField->State->GridFieldAddRelation = null;
return $dataList;
}
/**
*
* @param GridField $gridField
* @return array
*/
public function getURLHandlers($gridField) {
return array(
'search/$ID' => 'doSearch',
);
}
/**
* Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
*
* @param GridField $gridField
* @param SS_HTTPRequest $request
*/
public function doSearch($gridField, $request) {
$allList = DataList::create($gridField->getList()->dataClass());
$results = $allList->subtract($gridField->getList())->filter($this->fieldToSearch.':StartsWith',$request->param('ID'));
$results->sort($this->fieldToSearch, 'ASC');
$json = array();
foreach($results as $result) {
$json[$result->ID] = $result->{$this->fieldToSearch};
}
return Convert::array2json($json);
}
/**
* This will provide a StartsWith search that only returns a value if we are
* matching ONE object only. We wouldn't want to attach used any object to
* the list.
*
* @param GridField $gridField
* @param string $field
* @param string $searchTerm
* @param string $dataclass
* @return string
*/
protected function findSingleEntry($gridField, $field, $searchTerm, $dataclass) {
$fullList = DataList::create($dataclass);
$searchTerm = Convert::raw2sql($searchTerm);
if(!$searchTerm) {
return;
}
$existingList = clone $gridField->getList();
$searchResults = $fullList->subtract($existingList->limit(0))->filter($field.':StartsWith', $searchTerm);
// If more than one, skip
if($searchResults->count() != 1) {
return '';
}
$gridField->State->GridFieldAddRelation = $searchResults->first()->ID;
return $searchResults->first()->$field;
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* GridFieldRelationDelete
*
*/
class GridFieldRelationDelete implements GridField_ColumnProvider, GridField_ActionProvider {
/**
* Add a column 'UnlinkRelation'
*
* @param type $gridField
* @param array $columns
*/
public function augmentColumns($gridField, &$columns) {
$columns[] = 'UnlinkRelation';
}
/**
* Return any special attributes that will be used for FormField::createTag()
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return array
*/
public function getColumnAttributes($gridField, $record, $columnName) {
return array();
}
/**
* Don't add an title
*
* @param GridField $gridField
* @param string $columnName
* @return array
*/
public function getColumnMetadata($gridField, $columnName) {
if($columnName == 'UnlinkRelation') {
return array('title' => '');
}
}
/**
* Which columns are handled by this component
*
* @param type $gridField
* @return type
*/
public function getColumnsHandled($gridField) {
return array('UnlinkRelation');
}
/**
* Which GridField actions are this component handling
*
* @param GridField $gridField
* @return array
*/
public function getActions($gridField) {
return array('unlinkrelation');
}
/**
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return string - the HTML for the column
*/
public function getColumnContent($gridField, $record, $columnName) {
$field = new GridField_Action(
$gridField,
'UnlinkRelation'.$record->ID,
_t('GridAction.UnlinkRelation', "Unlink"),
"unlinkrelation",
array('RecordID' => $record->ID)
);
$output = $field->Field();
return $output;
}
/**
* Handle the actions and apply any changes to the GridField
*
* @param GridField $gridField
* @param string $actionName
* @param mixed $arguments
* @param array $data - form data
* @return void
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
$id = $arguments['RecordID'];
$item = $gridField->getList()->byID($id);
if(!$item) return;
if($actionName == 'unlinkrelation') {
$gridField->getList()->remove($item);
}
}
}

View File

@ -0,0 +1,36 @@
jQuery(function($){
$(document).delegate("#gridfield_relationsearch", "focus", function (event) {
$(this).autocomplete({
source: function(request, response){
var searchField = $(this.element);
var form = $(this.element).closest("form");
// Due to some very weird behaviout of jquery.metadata, the url have to be double quoted
var suggestionUrl = $(searchField).attr('data-search-url').substr(1,$(searchField).attr('data-search-url').length-2);
$.ajax({
headers: {
"X-Get-Fragment" : 'Partial'
},
type: "GET",
url: suggestionUrl+'/'+request.term,
data: form.serialize()+'&'+escape(searchField.attr('name'))+'='+escape(searchField.val()),
success: function(data) {
response( $.map(JSON.parse(data), function( name, id ) {
return { label: name, value: name, id: id }
}));
},
error: function(e) {
alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.'));
}
});
},
select: function(event, ui) {
$(this).closest("fieldset.ss-gridfield").find("#action_gridfield_relationfind").replaceWith(
'<input type="hidden" name="relationID" value="'+ui.item.id+'" id="relationID"/>'
);
$(this).closest("fieldset.ss-gridfield").find("#action_gridfield_relationadd").removeAttr('disabled');
}
});
});
});

18
security/Group.php Normal file → Executable file
View File

@ -62,17 +62,18 @@ class Group extends DataObject {
public function getCMSFields() { public function getCMSFields() {
Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js'); Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js');
$config = new GridFieldConfig_ManyManyEditor('FirstName', true, 20);
$config->addComponent(new GridFieldPopupForms(Controller::curr(), 'EditForm'));
$memberList = new GridField('Members','Members', $this->Members(), $config);
// @todo Implement permission checking on GridField
//$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
//$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
$fields = new FieldList( $fields = new FieldList(
new TabSet("Root", new TabSet("Root",
new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'), new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'),
new TextField("Title", $this->fieldLabel('Title')), new TextField("Title", $this->fieldLabel('Title')),
$memberList = new MemberTableField( $memberList
(Controller::has_curr()) ? Controller::curr() : new Controller(),
"Members",
$this,
null,
false
)
), ),
$permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'), $permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'),
@ -152,9 +153,6 @@ class Group extends DataObject {
$rolesField->setDisabledItems($inheritedRoles->column('ID')); $rolesField->setDisabledItems($inheritedRoles->column('ID'));
} }
$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
$fields->push($idField = new HiddenField("ID")); $fields->push($idField = new HiddenField("ID"));
$this->extend('updateCMSFields', $fields); $this->extend('updateCMSFields', $fields);

View File

@ -0,0 +1,3 @@
<a href="$Backlink"> Go back the way you came!</a>
$ItemEditForm

View File

@ -0,0 +1,4 @@
<div><% control Fields %>
<span>$Field</span>
<% end_control %>
</div>