From 5efeb5642f4eef9dbc4356caede336d616e7eddc Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Fri, 15 May 2015 22:29:17 +1200 Subject: [PATCH] Stabilized API --- README.md | 37 +++- code/StringTagField.php | 285 +++++++++++++++++++++++++++ code/TagField.php | 343 ++++++++++++++------------------ js/TagField.js | 18 +- tests/StringTagFieldTest.php | 149 ++++++++++++++ tests/StringTagFieldTest.yml | 6 + tests/TagFieldTest.php | 269 ++++++++++++++++++++++++++ tests/TagFieldTest.yml | 11 ++ tests/unit/TagFieldTest.php | 365 ----------------------------------- tests/unit/TagFieldTest.yml | 14 -- 10 files changed, 891 insertions(+), 606 deletions(-) create mode 100644 code/StringTagField.php create mode 100755 tests/StringTagFieldTest.php create mode 100755 tests/StringTagFieldTest.yml create mode 100755 tests/TagFieldTest.php create mode 100755 tests/TagFieldTest.yml delete mode 100755 tests/unit/TagFieldTest.php delete mode 100755 tests/unit/TagFieldTest.yml diff --git a/README.md b/README.md index 3e12c2a..c6c70b5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # TagField Module -[![Build Status](https://secure.travis-ci.org/silverstripe-labs/silverstripe-tagfield.png?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-tagfield) - -## Maintainer Contact - -* Christopher Pitt (Nickname: assertchris) +[![Build Status](http://img.shields.io/travis/silverstripe-labs/silverstripe-tagfield.svg?style=flat-square)](https://travis-ci.org/silverstripe-labs/silverstripe-tagfield) +[![Code Quality](http://img.shields.io/scrutinizer/g/silverstripe-labs/silverstripe-tagfield.svg?style=flat-square)](https://scrutinizer-ci.com/g/silverstripe-labs/silverstripe-tagfield) ## Requirements @@ -17,6 +14,8 @@ ## Usage +### Relational Tags + ```php class BlogPost extends DataObject { static $many_many = array( @@ -38,10 +37,28 @@ class BlogTag extends DataObject { ``` ```php -$all = BlogTags::get()->map(); -$linked = $post->BlogTags()->map(); - $field = new TagField( - 'BlogTags', 'Blog Tags', $all, $linked + 'BlogTags', 'Blog Tags', BlogTags::get(), $post->BlogTags() ); -``` \ No newline at end of file + +$field->setShouldLazyLoad(true); // tags should be lazy loaded +$field->setCanCreate(true); // new tag DataObjects can be created +``` + +### String Tags + +```php +class BlogPost extends DataObject { + static $db = array( + 'Tags' => 'Text' + ); +} +``` + +```php +$field = new StringTagField( + 'BlogTags', 'Blog Tags', array('one', 'two'), explode(',', $post->Tags) +); + +$field->setShouldLazyLoad(true); // tags should be lazy loaded +``` diff --git a/code/StringTagField.php b/code/StringTagField.php new file mode 100644 index 0000000..16f502a --- /dev/null +++ b/code/StringTagField.php @@ -0,0 +1,285 @@ +shouldLazyLoad; + } + + /** + * @param bool $shouldLazyLoad + * + * @return static + */ + public function setShouldLazyLoad($shouldLazyLoad) { + $this->shouldLazyLoad = $shouldLazyLoad; + + return $this; + } + + /** + * @return int + */ + public function getLazyLoadItemLimit() { + return $this->lazyLoadItemLimit; + } + + /** + * @param int $lazyLoadItemLimit + * + * @return static + */ + public function setLazyLoadItemLimit($lazyLoadItemLimit) { + $this->lazyLoadItemLimit = $lazyLoadItemLimit; + + return $this; + } + + /** + * @return null|DataObject + */ + public function getRecord() { + if($this->record) { + return $this->record; + } + + if($form = $this->getForm()) { + return $form->getRecord(); + } + + return null; + } + + /** + * @param DataObject $record + * + * @return $this + */ + public function setRecord(DataObject $record) { + $this->record = $record; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function Field($properties = array()) { + Requirements::css(TAG_FIELD_DIR . '/css/select2.min.css'); + Requirements::css(TAG_FIELD_DIR . '/css/TagField.css'); + + Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript(TAG_FIELD_DIR . '/js/select2.js'); + Requirements::javascript(TAG_FIELD_DIR . '/js/TagField.js'); + + $this->addExtraClass('ss-tag-field'); + + $this->setAttribute('multiple', 'multiple'); + + if($this->getShouldLazyLoad()) { + $this->setAttribute('data-ss-tag-field-suggest-url', $this->getSuggestURL()); + } else { + $properties = array_merge($properties, array( + 'Options' => $this->getOptions() + )); + } + + return $this + ->customise($properties) + ->renderWith(array("templates/StringTagField")); + } + + /** + * @return string + */ + protected function getSuggestURL() { + return Controller::join_links($this->Link(), 'suggest'); + } + + /** + * @return ArrayList + */ + protected function getOptions() { + $options = ArrayList::create(); + + $source = $this->getSource(); + + if($source instanceof Iterator) { + $source = iterator_to_array($source); + } + + $values = $this->Value(); + + foreach($source as $value) { + $options->push( + ArrayData::create(array( + 'Title' => $value, + 'Value' => $value, + 'Selected' => in_array($value, $values), + )) + ); + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $source = null) { + if(is_string($value)) { + $value = explode(',', $value); + } + + if($source instanceof DataObject) { + $name = $this->getName(); + $value = $source->$name; + } + + if($source instanceof SS_List) { + $value = $source->column('ID'); + } + + return parent::setValue(array_filter($value)); + } + + /** + * {@inheritdoc} + */ + public function getAttributes() { + return array_merge( + parent::getAttributes(), + array('name' => $this->getName() . '[]') + ); + } + + /** + * {@inheritdoc} + */ + public function saveInto(DataObjectInterface $record) { + parent::saveInto($record); + + $name = $this->getName(); + + $record->$name = join(',', $this->Value()); + $record->write(); + } + + /** + * Returns a JSON string of tags, for lazy loading. + * + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ + public function suggest(SS_HTTPRequest $request) { + $responseBody = Convert::raw2json( + array('items' => array()) + ); + + $response = new SS_HTTPResponse(); + $response->addHeader('Content-Type', 'application/json'); + + if($record = $this->getRecord()) { + $tags = array(); + $term = $request->getVar('term'); + + if($record->hasField($this->getName())) { + $tags = $this->getTags($term); + } + + $responseBody = Convert::raw2json( + array('items' => $tags) + ); + } + + $response->setBody($responseBody); + + return $response; + } + + /** + * Returns array of arrays representing tags. + * + * @param string $term + * + * @return array + */ + protected function getTags($term) { + $record = $this->getRecord(); + + if(!$record) { + return array(); + } + + $fieldName = $this->getName(); + $className = $record->getClassName(); + + $term = Convert::raw2sql($term); + + $query = $className::get() + ->filter($fieldName . ':PartialMatch:nocase', $term) + ->limit($this->getLazyLoadItemLimit()); + + $items = array(); + + foreach($query->column($fieldName) as $tags) { + $tags = explode(',', $tags); + + foreach($tags as $i => $tag) { + if(stripos($tag, $term) !== false && !in_array($tag, $items)) { + $items[] = array( + 'id' => $tag, + 'text' => $tag + ); + } + } + } + + return $items; + } +} diff --git a/code/TagField.php b/code/TagField.php index 015942c..7337896 100644 --- a/code/TagField.php +++ b/code/TagField.php @@ -1,10 +1,10 @@ setReadOnly($readOnly); - + public function __construct($name, $title = '', $source = null, $value = null) { parent::__construct($name, $title, $source, $value); } /** * @return bool */ - public function getAjax() { - return $this->ajax; + public function getShouldLazyLoad() { + return $this->shouldLazyLoad; } /** - * @param bool $ajax + * @param bool $shouldLazyLoad * * @return static */ - public function setAjax($ajax) { - $this->ajax = $ajax; - - return $this; - } - - /** - * @return bool - */ - public function getReadOnly() { - return $this->readOnly; - } - - /** - * @param bool $readOnly - * - * @return static - */ - public function setReadOnly($readOnly) { - $this->readOnly = $readOnly; - - return $this; - } - - /** - * @return null|string - */ - public function getRelationTitle() { - return $this->relationTitle; - } - - /** - * @param string $relationTitle - * - * @return static - */ - public function setRelationTitle($relationTitle) { - $this->relationTitle = $relationTitle; + public function setShouldLazyLoad($shouldLazyLoad) { + $this->shouldLazyLoad = $shouldLazyLoad; return $this; } @@ -109,35 +65,53 @@ class TagField extends DropdownField { /** * @return int */ - public function getAjaxItemLimit() { - return $this->ajaxItemLimit; + public function getLazyLoadItemLimit() { + return $this->lazyLoadItemLimit; } /** - * @param int $ajaxItemLimit + * @param int $lazyLoadItemLimit * * @return static */ - public function setAjaxItemLimit($ajaxItemLimit) { - $this->ajaxItemLimit = $ajaxItemLimit; + public function setLazyLoadItemLimit($lazyLoadItemLimit) { + $this->lazyLoadItemLimit = $lazyLoadItemLimit; return $this; } /** - * @return null|string + * @return bool */ - public function getRecordClass() { - return $this->recordClass; + public function getCanCreate() { + return $this->canCreate; } /** - * @param string $recordClass + * @param bool $canCreate * * @return static */ - public function setRecordClass($recordClass) { - $this->recordClass = $recordClass; + public function setCanCreate($canCreate) { + $this->canCreate = $canCreate; + + return $this; + } + + /** + * @return string + */ + public function getTitleField() { + return $this->titleField; + } + + /** + * @param string $titleField + * + * @return $this + */ + public function setTitleField($titleField) { + $this->titleField = $titleField; return $this; } @@ -149,23 +123,23 @@ class TagField extends DropdownField { Requirements::css(TAG_FIELD_DIR . '/css/select2.min.css'); Requirements::css(TAG_FIELD_DIR . '/css/TagField.css'); - Requirements::javascript(TAG_FIELD_DIR . '/js/TagField.js'); - Requirements::javascript(TAG_FIELD_DIR . '/js/select2.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript(TAG_FIELD_DIR . '/js/select2.js'); + Requirements::javascript(TAG_FIELD_DIR . '/js/TagField.js'); - $this->addExtraClass('silverstripe-tag-field'); + $this->addExtraClass('ss-tag-field'); $this->setAttribute('multiple', 'multiple'); - if($this->ajax) { - $this->setAttribute('data-suggest-url', $this->getSuggestURL()); + if($this->shouldLazyLoad) { + $this->setAttribute('data-ss-tag-field-suggest-url', $this->getSuggestURL()); + } else { + $properties = array_merge($properties, array( + 'Options' => $this->getOptions() + )); } - $properties = array_merge($properties, array( - 'Options' => $this->getOptions() - )); - return $this ->customise($properties) ->renderWith(array("templates/TagField")); @@ -186,18 +160,32 @@ class TagField extends DropdownField { $source = $this->getSource(); - if($source instanceof Iterator) { - $source = iterator_to_array($source); + if(!$source) { + $source = new ArrayList(); } + $dataClass = $source->dataClass(); + $values = $this->Value(); - foreach($source as $key => $value) { + if(!$values) { + return $options; + } + + if(is_array($values)) { + $values = DataList::create($dataClass)->filter('ID', $values); + } + + $ids = $values->column('ID'); + + $titleField = $this->getTitleField(); + + foreach($source as $object) { $options->push( ArrayData::create(array( - "Title" => $value, - "Value" => $key, - "Selected" => in_array($key, $values), + 'Title' => $object->$titleField, + 'Value' => $object->ID, + 'Selected' => in_array($object->ID, $ids), )) ); } @@ -208,20 +196,18 @@ class TagField extends DropdownField { /** * {@inheritdoc} */ - public function setValue($value, $record = null) { - if(empty($value) && $record) { - if($record instanceof DataObject) { - $name = $this->getName(); + public function setValue($value, $source = null) { + if($source instanceof DataObject) { + $name = $this->getName(); - if($record->hasMethod($name)) { - $value = $record->$name()->getIDList(); - } - } elseif($record instanceof SS_List) { - $value = $record->column('ID'); + if($source->hasMethod($name)) { + $value = $source->$name()->getIDList(); } + } elseif($value instanceof SS_List) { + $value = $value->column('ID'); } - return parent::setValue($value, $record); + return parent::setValue(array_filter($value)); } /** @@ -241,108 +227,96 @@ class TagField extends DropdownField { parent::saveInto($record); $name = $this->getName(); - $relationTitle = $this->getRelationTitle(); + $titleField = $this->getTitleField(); + + $source = $this->getSource(); + + $dataClass = $source->dataClass(); $values = $this->Value(); - if(empty($values) || empty($record) || empty($relationTitle)) { + if(!$values) { + $values = array(); + } + + if(empty($record) || empty($source) || empty($titleField)) { return; } - if($record->hasMethod($name)) { - $relation = $record->$name(); - - $class = $relation->dataClass(); - - foreach($values as $i => $value) { - if(!is_numeric($value)) { - if($this->getReadOnly()) { - unset($values[$i]); - continue; - } else { - $instance = new $class(); - $instance->{$relationTitle} = $value; - $instance->write(); - - $values[$i] = $instance->ID; - } - } - } - - $relation->setByIDList($values); - } else { - $record->$name = implode(',', $values); + if(!$record->hasMethod($name)) { + throw new Exception( + sprintf("%s does not have a %s method", get_class($record), $name) + ); } + + $relation = $record->$name(); + + foreach($values as $i => $value) { + if(!is_numeric($value)) { + if(!$this->getCanCreate()) { + unset($values[$i]); + continue; + } + + $record = new $dataClass(); + $record->{$titleField} = $value; + $record->write(); + + $values[$i] = $record->ID; + } + } + + if($values instanceof SS_List) { + $values = iterator_to_array($values); + } + + $relation->setByIDList(array_filter($values)); } /** - * Returns a JSON string of tags, for ajax-based search. + * Returns a JSON string of tags, for lazy loading. * * @param SS_HTTPRequest $request * * @return SS_HTTPResponse */ public function suggest(SS_HTTPRequest $request) { - $recordClass = $this->getRecordClass(); + $tags = $this->getTags($request->getVar('term')); $response = new SS_HTTPResponse(); - $response->addHeader('Content-Type', 'application/json'); - - $response->setBody(Convert::raw2json( - array('items' => array()) - )); - - if($recordClass !== null) { - $name = $this->getName(); - - /** - * @var DataObject $object - */ - $object = singleton($recordClass); - - $term = $request->getVar('term'); - - $tags = array(); - - if($object->hasMethod($name)) { - $tags = $this->getObjectTags($object, $term); - } elseif($object->hasField($name)) { - $tags = $this->getStringTags($term); - } - - $response->setBody(Convert::raw2json( - array('items' => $tags) - )); - } + $response->setBody(json_encode(array('items' => $tags))); return $response; } /** - * Returns array of arrays representing DataObject-based tags. + * Returns array of arrays representing tags. * - * @param DataObject $instance * @param string $term * * @return array */ - protected function getObjectTags(DataObject $instance, $term) { - $name = $this->getName(); - $relationTitle = $this->getRelationTitle(); + protected function getTags($term) { + /** + * @var DataList $source + */ + $source = $this->getSource(); - $relation = $instance->{$name}(); + $dataClass = $source->dataClass(); + + $titleField = $this->getTitleField(); $term = Convert::raw2sql($term); - $query = DataList::create($relation->dataClass()) - ->filter($relationTitle . ':PartialMatch:nocase', $term) - ->sort($relationTitle) - ->limit($this->getAjaxItemLimit()); + $query = $dataClass::get() + ->filter($titleField . ':PartialMatch:nocase', $term) + ->sort($titleField) + ->limit($this->getLazyLoadItemLimit()); $items = array(); - foreach($query->map('ID', $relationTitle) as $id => $title) { + foreach($query->map('ID', $titleField) as $id => $title) { if(!in_array($title, $items)) { $items[] = array( 'id' => $id, @@ -353,39 +327,4 @@ class TagField extends DropdownField { return $items; } - - /** - * Returns array of arrays representing string-based tags. - * - * @param string $term - * - * @return array - */ - protected function getStringTags($term) { - $name = $this->getName(); - $recordClass = $this->getRecordClass(); - - $term = Convert::raw2sql($term); - - $query = DataObject::get($recordClass) - ->filter($name . ':PartialMatch:nocase', $term) - ->limit($this->getAjaxItemLimit()); - - $items = array(); - - foreach($query->column($name) as $tags) { - $tags = explode(',', $tags); - - foreach($tags as $i => $tag) { - if(stripos($tag, $term) !== false && !in_array($tag, $items)) { - $items[] = array( - 'id' => $tag, - 'text' => $tag - ); - } - } - } - - return $items; - } } diff --git a/js/TagField.js b/js/TagField.js index 3f0b6ef..22f1f88 100644 --- a/js/TagField.js +++ b/js/TagField.js @@ -21,7 +21,7 @@ $.entwine('ss', function ($) { - $('.silverstripe-tag-field + .chzn-container').entwine({ + $('.ss-tag-field + .chzn-container').entwine({ applySelect2: function () { var self = this, $select = $(this).prev(); @@ -40,9 +40,9 @@ 'tokenSeparators': [',', ' '] }; - if ($select.attr('data-suggest-url')) { + if ($select.attr('data-ss-tag-field-suggest-url')) { options.ajax = { - 'url': $select.attr('data-suggest-url'), + 'url': $select.attr('data-ss-tag-field-suggest-url'), 'dataType': 'json', 'delay': 250, 'data': function (params) { @@ -62,18 +62,6 @@ $select .chosenDestroy() .select2(options); - - /* - * Delay a cycle so select2 is initialised before - * selecting values (if data-selected-values is present). - */ - setTimeout(function () { - if ($select.attr('data-selected-values')) { - var values = $select.attr('data-selected-values'); - - $select.select2('val', values.split(',')); - } - }, 0); }, onmatch: function () { this.applySelect2(); diff --git a/tests/StringTagFieldTest.php b/tests/StringTagFieldTest.php new file mode 100755 index 0000000..bc34aa2 --- /dev/null +++ b/tests/StringTagFieldTest.php @@ -0,0 +1,149 @@ +getNewStringTagFieldTestBlogPost('BlogPost1'); + + $field = new StringTagField('Tags'); + $field->setValue(array('Tag1', 'Tag2')); + $field->saveInto($record); + + $record->write(); + + $this->assertEquals('Tag1,Tag2', $record->Tags); + } + + /** + * @param string $name + * + * @return StringTagFieldTestBlogPost + */ + protected function getNewStringTagFieldTestBlogPost($name) { + return $this->objFromFixture( + 'StringTagFieldTestBlogPost', + $name + ); + } + + function testItSavesTagsOnExistingRecords() { + $record = $this->getNewStringTagFieldTestBlogPost('BlogPost1'); + $record->write(); + + $field = new StringTagField('Tags'); + $field->setValue(array('Tag1', 'Tag2')); + $field->saveInto($record); + + $this->assertEquals('Tag1,Tag2', $record->Tags); + } + + function testItSuggestsTags() { + $record = $this->getNewStringTagFieldTestBlogPost('BlogPost2'); + + $field = new StringTagField('Tags'); + $field->setRecord($record); + + /** + * Partial tag title match. + */ + $request = $this->getNewRequest(array('term' => 'Tag')); + + $this->assertEquals( + '{"items":[{"id":"Tag1","text":"Tag1"},{"id":"Tag2","text":"Tag2"}]}', + $field->suggest($request)->getBody() + ); + + /** + * Exact tag title match. + */ + $request = $this->getNewRequest(array('term' => 'Tag1')); + + $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":"Tag1","text":"Tag1"}]}'); + + /** + * Case-insensitive tag title match. + */ + $request = $this->getNewRequest(array('term' => 'TAG1')); + + $this->assertEquals( + '{"items":[{"id":"Tag1","text":"Tag1"}]}', + $field->suggest($request)->getBody() + ); + + /** + * No tag title match. + */ + $request = $this->getNewRequest(array('term' => 'unknown')); + + $this->assertEquals( + '{"items":[]}', + $field->suggest($request)->getBody() + ); + } + + /** + * @param array $parameters + * + * @return SS_HTTPRequest + */ + protected function getNewRequest(array $parameters) { + return new SS_HTTPRequest( + 'get', + 'StringTagFieldTestController/StringTagFieldTestForm/fields/Tags/suggest', + $parameters + ); + } +} + +/** + * @property string $Tags + */ +class StringTagFieldTestBlogPost extends DataObject implements TestOnly { + /** + * @var array + */ + private static $db = array( + 'Title' => 'Text', + 'Content' => 'Text', + 'Tags' => 'Text', + ); +} + +class StringTagFieldTestController extends Controller implements TestOnly { + /** + * @return Form + */ + public function StringTagFieldTestForm() { + $fields = new FieldList( + $tagField = new StringTagField('Tags') + ); + + $actions = new FieldList( + new FormAction('StringTagFieldTestFormSubmit') + ); + + return new Form($this, 'StringTagFieldTestForm', $fields, $actions); + } + + /** + * @param DataObject $dataObject + * @param Form $form + */ + public function StringTagFieldTestFormSubmit(DataObject $dataObject, Form $form) { + $form->saveInto($dataObject); + } +} diff --git a/tests/StringTagFieldTest.yml b/tests/StringTagFieldTest.yml new file mode 100755 index 0000000..f69456e --- /dev/null +++ b/tests/StringTagFieldTest.yml @@ -0,0 +1,6 @@ +StringTagFieldTestBlogPost: + BlogPost1: + Title: BlogPost1 + BlogPost2: + Title: BlogPost2 + Tags: Tag1,Tag2 \ No newline at end of file diff --git a/tests/TagFieldTest.php b/tests/TagFieldTest.php new file mode 100755 index 0000000..ecc9620 --- /dev/null +++ b/tests/TagFieldTest.php @@ -0,0 +1,269 @@ +getNewTagFieldTestBlogPost('BlogPost1'); + + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')); + $field->setValue(array('Tag3', 'Tag4')); + $field->saveInto($record); + + $record->write(); + + $this->compareExpectedAndActualTags( + array('Tag3', 'Tag4'), + $record + ); + } + + /** + * @param string $name + * + * @return TagFieldTestBlogPost + */ + protected function getNewTagFieldTestBlogPost($name) { + return $this->objFromFixture( + 'TagFieldTestBlogPost', + $name + ); + } + + /** + * @param array $expected + * @param TagFieldTestBlogPost $record + */ + protected function compareExpectedAndActualTags(array $expected, TagFieldTestBlogPost $record) { + $actual = array_values($record->Tags()->map('ID', 'Title')->toArray()); + + sort($expected); + sort($actual); + + $this->assertEquals( + $expected, + $actual + ); + } + + public function testItSavesLinksToNewTagsOnExistingRecords() { + $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); + $record->write(); + + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')); + $field->setValue(array('Tag3', 'Tag4')); + $field->saveInto($record); + + $this->compareExpectedAndActualTags( + array('Tag3', 'Tag4'), + $record + ); + } + + public function testItSavesLinksToExistingTagsOnNewRecords() { + $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); + + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')); + $field->setValue(array('Tag1', 'Tag2')); + $field->saveInto($record); + + $record->write(); + + $this->compareExpectedAndActualTags( + array('Tag1', 'Tag2'), + $record + ); + } + + public function testItSavesLinksToExistingTagsOnExistingRecords() { + $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); + $record->write(); + + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')); + $field->setValue(array('Tag1', 'Tag2')); + $field->saveInto($record); + + $this->compareExpectedAndActualTags( + array('Tag1', 'Tag2'), + $record + ); + } + + function testItSuggestsTags() { + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')); + + /** + * Partial tag title match. + */ + $request = $this->getNewRequest(array('term' => 'Tag')); + + $this->assertEquals( + '{"items":[{"id":1,"text":"Tag1"},{"id":2,"text":"Tag2"}]}', + $field->suggest($request)->getBody() + ); + + /** + * Exact tag title match. + */ + $request = $this->getNewRequest(array('term' => 'Tag1')); + + $this->assertEquals( + '{"items":[{"id":1,"text":"Tag1"}]}', + $field->suggest($request)->getBody() + ); + + /** + * Case-insensitive tag title match. + */ + $request = $this->getNewRequest(array('term' => 'TAG1')); + + $this->assertEquals( + '{"items":[{"id":1,"text":"Tag1"}]}', + $field->suggest($request)->getBody() + ); + + /** + * No tag title match. + */ + $request = $this->getNewRequest(array('term' => 'unknown')); + + $this->assertEquals( + '{"items":[]}', + $field->suggest($request)->getBody() + ); + } + + /** + * @param array $parameters + * + * @return SS_HTTPRequest + */ + protected function getNewRequest(array $parameters) { + return new SS_HTTPRequest( + 'get', + 'TagFieldTestController/TagFieldTestForm/fields/Tags/suggest', + $parameters + ); + } + + function testItDisplaysValuesFromRelations() { + $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); + $record->write(); + + $form = new Form( + new TagFieldTestController($record), + 'Form', + new FieldList( + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')) + ), + new FieldList() + ); + + $form->loadDataFrom( + $this->objFromFixture('TagFieldTestBlogPost', 'BlogPost2') + ); + + $this->assertEquals($field->Value(), array(1 => 1, 2 => 2)); + } + + function testItIgnoresNewTagsIfCannotCreate() { + $record = new TagFieldTestBlogPost(); + $record->write(); + + $tag = TagFieldTestBlogTag::get()->filter('Title', 'Tag1')->first(); + + $field = new TagField('Tags', '', new DataList('TagFieldTestBlogTag'), array($tag->ID, 'Tag3')); + $field->setCanCreate(false); + $field->saveInto($record); + + /** + * @var TagFieldTestBlogPost $record + */ + $record = DataObject::get_by_id('TagFieldTestBlogPost', $record->ID); + + $this->compareExpectedAndActualTags( + array('Tag1'), + $record + ); + } +} + +class TagFieldTestBlogTag extends DataObject implements TestOnly { + /** + * @var string + */ + private static $default_sort = '"TagFieldTestBlogTag"."ID" ASC'; + + /** + * @var array + */ + private static $db = array( + 'Title' => 'Varchar(200)', + ); + + /** + * @var array + */ + private static $belongs_many_many = array( + 'BlogPosts' => 'TagFieldTestBlogPost', + ); +} + +/** + * @method ManyManyList Tags() + */ +class TagFieldTestBlogPost extends DataObject implements TestOnly { + /** + * @var array + */ + private static $db = array( + 'Title' => 'Text', + 'Content' => 'Text', + ); + + /** + * @var array + */ + private static $many_many = array( + 'Tags' => 'TagFieldTestBlogTag', + ); +} + +class TagFieldTestController extends Controller implements TestOnly { + /** + * @return Form + */ + public function TagFieldTestForm() { + $fields = new FieldList( + $tagField = new TagField('Tags', '', new DataList('TagFieldTestBlogTag')) + ); + + $actions = new FieldList( + new FormAction('TagFieldTestFormSubmit') + ); + + return new Form($this, 'TagFieldTestForm', $fields, $actions); + } + + /** + * @param DataObject $dataObject + * @param Form $form + */ + public function TagFieldTestFormSubmit(DataObject $dataObject, Form $form) { + $form->saveInto($dataObject); + } +} diff --git a/tests/TagFieldTest.yml b/tests/TagFieldTest.yml new file mode 100755 index 0000000..c69972d --- /dev/null +++ b/tests/TagFieldTest.yml @@ -0,0 +1,11 @@ +TagFieldTestBlogTag: + Tag1: + Title: Tag1 + Tag2: + Title: Tag2 +TagFieldTestBlogPost: + BlogPost1: + Title: BlogPost1 + BlogPost2: + Title: BlogPost2 + Tags: =>TagFieldTestBlogTag.Tag1,=>TagFieldTestBlogTag.Tag2 diff --git a/tests/unit/TagFieldTest.php b/tests/unit/TagFieldTest.php deleted file mode 100755 index 775b0d5..0000000 --- a/tests/unit/TagFieldTest.php +++ /dev/null @@ -1,365 +0,0 @@ -objFromFixture( - 'TagFieldTest_BlogPost', - 'BlogPost1' - ); - - $field = new TagField('Tags'); - $field->setValue(array('Object1', 'Object2')); - $field->saveInto($record); - - $record->write(); - - $this->compareExpectedAndActualTags( - $record, - array('Object1', 'Object2') - ); - } - - /** - * @param DataObject $record - * @param array $expected - */ - protected function compareExpectedAndActualTags(DataObject $record, array $expected) { - $compare1 = array_values($record->Tags()->map('ID', 'Title')->toArray()); - $compare2 = $expected; - - sort($compare1); - sort($compare2); - - $this->assertEquals( - $compare1, - $compare2 - ); - } - - public function testItSavesLinksToExistingTagsOnNewRecords() { - $record = new TagFieldTest_BlogPost(); - $record->write(); - - $field = new TagField('Tags'); - $field->setValue(array('Object1', 'Object2')); - $field->saveInto($record); - - $record->write(); - - $this->compareExpectedAndActualTags( - $record, - array('Object1', 'Object2') - ); - } - - public function testItSavesLinksToNewTagsOnExistingRecords() { - $record = $this->objFromFixture( - 'TagFieldTest_BlogPost', - 'BlogPost1' - ); - - $field = new TagField('Tags'); - $field->setValue(array('Object3', 'Object4')); - $field->saveInto($record); - - $record->write(); - - $this->compareExpectedAndActualTags( - $record, - array('Object3', 'Object4') - ); - } - - function testItSavesLinksToNewTagsOnNewRecords() { - $record = new TagFieldTest_BlogPost(); - $record->write(); - - $field = new TagField('Tags'); - $field->setValue(array('Object3', 'Object4')); - $field->saveInto($record); - - $this->compareExpectedAndActualTags( - $record, - array('Object3', 'Object4') - ); - } - - function testItSavesTextBasedTagsOnExistingRecords() { - $record = $this->objFromFixture( - 'TagFieldTest_BlogPost', - 'BlogPost1' - ); - - $field = new TagField('TextBasedTags'); - $field->setValue(array('Text1', 'Text2')); - $field->saveInto($record); - - $record->write(); - - $this->assertEquals( - $record->TextBasedTags, - 'Text1,Text2' - ); - } - - function testItSavesTextBasedTagsOnNewRecords() { - $record = new TagFieldTest_BlogPost(); - $record->write(); - - $field = new TagField('TextBasedTags'); - $field->setValue(array('Text1', 'Text2')); - $field->saveInto($record); - - $record->write(); - - $this->assertEquals( - $record->TextBasedTags, - 'Text1,Text2' - ); - } - - function testItSuggestsObjectTags() { - $field = new TagField('Tags'); - $field->setRecordClass('TagFieldTest_BlogPost'); - - /** - * Partial tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/ObjectTestForm/fields/Tags/suggest', - array('term' => 'Object') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":1,"text":"Object1"},{"id":2,"text":"Object2"}]}'); - - /** - * Exact tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/ObjectTestForm/fields/Tags/suggest', - array('term' => 'Object1') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":1,"text":"Object1"}]}'); - - /** - * Case-insensitive tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/ObjectTestForm/fields/Tags/suggest', - array('term' => 'OBJECT1') - ); - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":1,"text":"Object1"}]}'); - - /** - * No tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/ObjectTestForm/fields/Tags/suggest', - array('term' => 'unknown') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[]}'); - } - - function testItSuggestsTextTags() { - $field = new TagField('TextBasedTags'); - $field->setRecordClass('TagFieldTest_BlogPost'); - - /** - * Partial tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/TextBasedTestForm/fields/Tags/suggest', - array('term' => 'Text') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":"Text1","text":"Text1"},{"id":"Text2","text":"Text2"}]}'); - - /** - * Exact tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/TextBasedTestForm/fields/Tags/suggest', - array('term' => 'Text1') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":"Text1","text":"Text1"}]}'); - - /** - * Case-insensitive tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/TextBasedTestForm/fields/Tags/suggest', - array('term' => 'TEXT1') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[{"id":"Text1","text":"Text1"}]}'); - - /** - * No tag title match. - */ - $request = new SS_HTTPRequest( - 'get', - 'TagFieldTest_Controller/TextBasedTestForm/fields/Tags/suggest', - array('term' => 'unknown') - ); - - $this->assertEquals($field->suggest($request)->getBody(), '{"items":[]}'); - } - - function testItDisplaysValuesFromRelations() { - $form = new Form( - $this, - 'Form', - new FieldList( - $field = new TagField('Tags') - ), - new FieldList() - ); - - $form->loadDataFrom( - $this->objFromFixture('TagFieldTest_BlogPost', 'BlogPost3') - ); - - $this->assertEquals($field->Value(), array(1 => 1, 2 => 2)); - } - - function testItIgnoresNewTagsIfReadOnly() { - $record = new TagFieldTest_BlogPost(); - $record->write(); - - $tag = TagFieldTest_BlogTag::get()->filter('Title', 'Object1')->first(); - - $field = new TagField('Tags'); - $field->setReadOnly(true); - $field->setValue(array($tag->ID, 'Object3')); - $field->saveInto($record); - - $record = DataObject::get_by_id('TagFieldTest_BlogPost', $record->ID); - - $this->compareExpectedAndActualTags( - $record, - array('Object1') - ); - } -} - -class TagFieldTest_BlogTag extends DataObject implements TestOnly { - /** - * @var string - */ - private static $default_sort = '"TagFieldTest_BlogTag"."ID" ASC'; - - /** - * @var array - */ - private static $db = array( - 'Title' => 'Varchar(200)', - ); - - /** - * @var array - */ - private static $belongs_many_many = array( - 'BlogEntries' => 'TagFieldTest_BlogPost', - ); -} - -class TagFieldTest_BlogPost extends DataObject implements TestOnly { - /** - * @var string - */ - private static $default_sort = '"TagFieldTest_BlogPost"."ID" ASC'; - - /** - * @var array - */ - private static $db = array( - 'Title' => 'Text', - 'Content' => 'Text', - 'TextBasedTags' => 'Text', - ); - - /** - * @var array - */ - private static $many_many = array( - 'Tags' => 'TagFieldTest_BlogTag', - ); -} - -class TagFieldTest_Controller extends Controller implements TestOnly { - /** - * @var array - */ - private static $url_handlers = array( - '$Action//$ID/$OtherID' => "handleAction", - ); - - /** - * @return Form - */ - public function ObjectTestForm() { - $fields = new FieldList( - $tagField = new TagField('Tags') - ); - - $actions = new FieldList( - new FormAction('ObjectTestForm_submit') - ); - - return new Form($this, 'ObjectTestForm', $fields, $actions); - } - - /** - * @param array $data - * @param Form $form - */ - public function ObjectTestForm_submit(array $data, Form $form) { - $data->saveInto($form); - } - - /** - * @return Form - */ - public function TextBasedTestForm() { - $fields = new FieldList( - $tagField = new TagField('TextBasedTags') - ); - - $actions = new FieldList( - new FormAction('TextBasedTestForm_submit') - ); - - return new Form($this, 'TextBasedTestForm', $fields, $actions); - } - - /** - * @param array $data - * @param Form $form - */ - public function TextBasedTestForm_submit(array $data, Form $form) { - $data->saveInto($form); - } -} diff --git a/tests/unit/TagFieldTest.yml b/tests/unit/TagFieldTest.yml deleted file mode 100755 index 0b832ce..0000000 --- a/tests/unit/TagFieldTest.yml +++ /dev/null @@ -1,14 +0,0 @@ -TagFieldTest_BlogTag: - Object1: - Title: Object1 - Object2: - Title: Object2 -TagFieldTest_BlogPost: - BlogPost1: - Title: BlogPost1 - BlogPost2: - Title: BlogPost2 - TextBasedTags: Text1,Text2 - BlogPost3: - Title: BlogPost3 - Tags: =>TagFieldTest_BlogTag.Object1,=>TagFieldTest_BlogTag.Object2