mirror of
https://github.com/silverstripe/silverstripe-tagfield
synced 2024-10-22 11:05:32 +02:00
added "tagfield" module
This commit is contained in:
commit
aa0fb00c7a
0
tagfield/CHANGELOG
Normal file
0
tagfield/CHANGELOG
Normal file
0
tagfield/LICENSE
Normal file
0
tagfield/LICENSE
Normal file
0
tagfield/README
Normal file
0
tagfield/README
Normal file
2
tagfield/_config.php
Normal file
2
tagfield/_config.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
?>
|
212
tagfield/code/TagField.php
Normal file
212
tagfield/code/TagField.php
Normal file
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* Features:
|
||||
* - Bundled with jQuery-based autocomplete library which is applied to a textfield
|
||||
* - Autosuggest functionality (currently JSON only)
|
||||
* @author Ingo Schommer, SilverStripe Ltd. (<firstname>@silverstripe.com)
|
||||
* @package formfields
|
||||
* @subpackage tagfield
|
||||
*/
|
||||
class TagField extends TextField {
|
||||
|
||||
/**
|
||||
* @var string $tagTextbasedClass The DataObject class with a text property
|
||||
* or many_many relation matching the name of the Field.
|
||||
*/
|
||||
protected $tagTopicClass;
|
||||
|
||||
/**
|
||||
* @var string $tagObjectField Only applies to object-based tagging.
|
||||
* The fieldname for textbased tagging is inferred from the formfield name.
|
||||
*/
|
||||
protected $tagObjectField = 'Title';
|
||||
|
||||
/**
|
||||
* @var string $tagFilter
|
||||
*/
|
||||
protected $tagFilter;
|
||||
|
||||
/**
|
||||
* @var string $tagSort If {@link suggest()} finds multiple matches, in which order should they
|
||||
* be presented.
|
||||
*/
|
||||
protected $tagSort;
|
||||
|
||||
/**
|
||||
* @var $separator
|
||||
*/
|
||||
protected $separator = ' ';
|
||||
|
||||
/**
|
||||
* @var array $customTags Override the tagging behaviour with a custom set
|
||||
* used by {@link suggest()}.
|
||||
*/
|
||||
protected $customTags;
|
||||
|
||||
function __construct($name, $title = null, $value = null, $tagTopicClass) {
|
||||
$this->tagTopicClass = $tagTopicClass;
|
||||
|
||||
parent::__construct($name, $title, $value);
|
||||
}
|
||||
|
||||
public function Field() {
|
||||
Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
|
||||
Requirements::javascript("tagfield/javascript/jquery.tags.js");
|
||||
Requirements::customScript("$.ready(function() {
|
||||
$('#" . $this->id() . "').tagSuggest({
|
||||
url: '" . $this->Link() . "/suggest'
|
||||
});
|
||||
});");
|
||||
|
||||
return parent::Field();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string JSON array
|
||||
*/
|
||||
public function suggest($request) {
|
||||
$tagTopicClassObj = singleton($this->tagTopicClass);
|
||||
|
||||
$searchString = $request->requestVar($this->Name());
|
||||
|
||||
if($this->customTags) {
|
||||
$tags = $this->customTags;
|
||||
} else if($tagTopicClassObj->many_many($this->Name())) {
|
||||
$tags = $this->getObjectTags($searchString);
|
||||
} else if($tagTopicClassObj->hasField($this->Name())) {
|
||||
$tags = $this->getTextbasedTags($searchString);
|
||||
} else {
|
||||
user_error('TagField::suggest(): Cant find valid relation or text property with name "' . $this->Name . '"', E_USER_ERROR);
|
||||
}
|
||||
|
||||
return Convert::raw2json($tags);
|
||||
}
|
||||
|
||||
function saveInto($record) {
|
||||
if($this->value) {
|
||||
// $record should match the $tagTopicClass
|
||||
if($record->many_many($this->Name()) {
|
||||
$this->saveIntoObjectTags($record);
|
||||
} elseif($record->hasField($this->Name())) {
|
||||
$this->saveIntoTextbasedTags($record);
|
||||
} else {
|
||||
user_error('TagField::saveInto(): Cant find valid field or relation to save into', E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveIntoObjectTags($record) {
|
||||
$tagsArr = $this->splitTagsToArray($this->value);
|
||||
|
||||
$relationName = $this->Name();
|
||||
$existingTagsComponentSet = $record->$relationName();
|
||||
$tagClass = $this->getTagClass();
|
||||
$tagBaseClass = ClassInfo::baseDataClass($tagClass);
|
||||
|
||||
$tagsToAdd = array();
|
||||
if($tagsArr) foreach($tagsArr as $tagString) {
|
||||
$SQL_filter = sprintf('`%s`.`%s` = "%s"',
|
||||
$tagBaseClass,
|
||||
$this->tagObjectField,
|
||||
Convert::raw2sql($tagString)
|
||||
);
|
||||
$tagObj = DataObject::get_one($tagClass, $SQL_filter);
|
||||
if(!$tagObj) {
|
||||
$tagObj = new $tagClass();
|
||||
$tagObj->{$this->tagObjectField} = $this->value;
|
||||
$tabObj->write();
|
||||
}
|
||||
$tagsToAdd[] = $tagObj;
|
||||
}
|
||||
|
||||
// remove all before readding
|
||||
$existingTagsComponentSet->removeAll();
|
||||
$existingTagsComponentSet->addMany($tagsToAdd);
|
||||
}
|
||||
|
||||
protected function saveIntoTextbasedTags($record) {
|
||||
$tagFieldName = $this->Name();
|
||||
$record->$tagFieldName = $this->value;
|
||||
}
|
||||
|
||||
protected function splitTagsToArray($tagsString) {
|
||||
return array_unique(split("*" . escape($this->separator) . "*", trim($tagsString)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use only when storing tags in objects
|
||||
*/
|
||||
protected function getTagClass() {
|
||||
$tagManyMany = singleton($this->tagTopicClass)->many_many($this->Name());
|
||||
if(!$tagManyMany) {
|
||||
user_error('TagField::getTagClass(): Cant find relation with name "' . $this->Name() . '"', E_USER_ERROR);
|
||||
}
|
||||
|
||||
return $tagManyMany[1];
|
||||
}
|
||||
|
||||
protected function getObjectTags($searchString) {
|
||||
$tagClass = $this->getTagClass();
|
||||
$tagBaseClass = ClassInfo::baseDataClass($tagClass);
|
||||
|
||||
$SQL_filter = sprintf("`%s`.`%s` LIKE '%%%s%%'",
|
||||
$tagBaseClass,
|
||||
$this->tagObjectField,
|
||||
Convert::raw2sql($searchString)
|
||||
);
|
||||
if($this->tagFilter) $SQL_filter .= ' AND ' . $this->tagFilter;
|
||||
|
||||
$tagObjs = DataObject::get($tagClass, $SQL_filter, $this->tagSort);
|
||||
$tagArr = ($tagObjs) ? array_values($tagObjs->map('ID', $this->tagObjectField)) : array();
|
||||
|
||||
return $tagArr;
|
||||
}
|
||||
|
||||
protected function getTextbasedTags($searchString) {
|
||||
$baseClass = ClassInfo::baseDataClass($this->tagTopicClass);
|
||||
|
||||
$SQL_filter = sprintf("`%s`.`%s` LIKE '%%%s%%'",
|
||||
$baseClass,
|
||||
$this->Name(),
|
||||
Convert::raw2sql($searchString)
|
||||
);
|
||||
if($this->tagFilter) $SQL_filter .= ' AND ' . $this->tagFilter;
|
||||
|
||||
$allTopicObjs = DataObject::get($this->tagTopicClass, $SQL_filter, $this->tagSort);
|
||||
$multipleTagsArr = ($allTopicObjs) ? array_values($allTopicObjs->map('ID', $this->Name)) : array();
|
||||
$tagArr = array();
|
||||
foreach($multipleTagsArr as $multipleTags) {
|
||||
$tagArr += $this->splitTagsToArray($multipleTags);
|
||||
}
|
||||
// remove duplicates (retains case sensitive duplicates)
|
||||
$tagArr = array_unique($tagArr);
|
||||
|
||||
return $tagArr;
|
||||
}
|
||||
|
||||
public function setTagFilter($sql) {
|
||||
$this->tagFilter = $sql;
|
||||
}
|
||||
|
||||
public function getTagFilter() {
|
||||
return $this->tagFilter;
|
||||
}
|
||||
|
||||
public function setTagSort($sql) {
|
||||
$this->tagSort = $sql;
|
||||
}
|
||||
|
||||
public function getTagSort() {
|
||||
return $this->tagSort;
|
||||
}
|
||||
|
||||
public function setSeparator($separator) {
|
||||
$this->separator = $separator;
|
||||
}
|
||||
|
||||
public function getSeparator() {
|
||||
return $this->separator;
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
267
tagfield/javascript/jquery.tags.js
Normal file
267
tagfield/javascript/jquery.tags.js
Normal file
@ -0,0 +1,267 @@
|
||||
/*
|
||||
@author: remy sharp / http://remysharp.com
|
||||
@url: http://remysharp.com/2007/12/28/jquery-tag-suggestion/
|
||||
@usage: setGlobalTags(['javascript', 'jquery', 'java', 'json']); // applied tags to be used for all implementations
|
||||
$('input.tags').tagSuggest(options);
|
||||
|
||||
The selector is the element that the user enters their tag list
|
||||
@params:
|
||||
matchClass - class applied to the suggestions, defaults to 'tagMatches'
|
||||
tagContainer - the type of element uses to contain the suggestions, defaults to 'span'
|
||||
tagWrap - the type of element the suggestions a wrapped in, defaults to 'span'
|
||||
sort - boolean to force the sorted order of suggestions, defaults to false
|
||||
url - optional url to get suggestions if setGlobalTags isn't used. Must return array of suggested tags
|
||||
tags - optional array of tags specific to this instance of element matches
|
||||
delay - optional sets the delay between keyup and the request - can help throttle ajax requests, defaults to zero delay
|
||||
separator - optional separator string, defaults to ' ' (Brian J. Cardiff)
|
||||
@license: Creative Commons License - ShareAlike http://creativecommons.org/licenses/by-sa/3.0/
|
||||
@version: 1.4
|
||||
@changes: fixed filtering to ajax hits
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
var globalTags = [];
|
||||
|
||||
// creates a public function within our private code.
|
||||
// tags can either be an array of strings OR
|
||||
// array of objects containing a 'tag' attribute
|
||||
window.setGlobalTags = function(tags /* array */) {
|
||||
globalTags = getTags(tags);
|
||||
};
|
||||
|
||||
function getTags(tags) {
|
||||
var tag, i, goodTags = [];
|
||||
for (i = 0; i < tags.length; i++) {
|
||||
tag = tags[i];
|
||||
if (typeof tags[i] == 'object') {
|
||||
tag = tags[i].tag;
|
||||
}
|
||||
goodTags.push(tag.toLowerCase());
|
||||
}
|
||||
|
||||
return goodTags;
|
||||
}
|
||||
|
||||
$.fn.tagSuggest = function (options) {
|
||||
var defaults = {
|
||||
'matchClass' : 'tagMatches',
|
||||
'tagContainer' : 'span',
|
||||
'tagWrap' : 'span',
|
||||
'sort' : true,
|
||||
'tags' : null,
|
||||
'url' : null,
|
||||
'delay' : 0,
|
||||
'separator' : ' '
|
||||
};
|
||||
|
||||
var i, tag, userTags = [], settings = $.extend({}, defaults, options);
|
||||
|
||||
if (settings.tags) {
|
||||
userTags = getTags(settings.tags);
|
||||
} else {
|
||||
userTags = globalTags;
|
||||
}
|
||||
|
||||
return this.each(function () {
|
||||
var tagsElm = $(this);
|
||||
var elm = this;
|
||||
var matches, fromTab = false;
|
||||
var suggestionsShow = false;
|
||||
var workingTags = [];
|
||||
var currentTag = {"position": 0, tag: ""};
|
||||
var tagMatches = document.createElement(settings.tagContainer);
|
||||
|
||||
function showSuggestionsDelayed(el, key) {
|
||||
if (settings.delay) {
|
||||
if (elm.timer) clearTimeout(elm.timer);
|
||||
elm.timer = setTimeout(function () {
|
||||
showSuggestions(el, key);
|
||||
}, settings.delay);
|
||||
} else {
|
||||
showSuggestions(el, key);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuggestions(el, key) {
|
||||
workingTags = el.value.split(settings.separator);
|
||||
matches = [];
|
||||
var i, html = '', chosenTags = {}, tagSelected = false;
|
||||
|
||||
// we're looking to complete the tag on currentTag.position (to start with)
|
||||
currentTag = { position: currentTags.length-1, tag: '' };
|
||||
|
||||
for (i = 0; i < currentTags.length && i < workingTags.length; i++) {
|
||||
if (!tagSelected &&
|
||||
currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
|
||||
currentTag = { position: i, tag: workingTags[i].toLowerCase() };
|
||||
tagSelected = true;
|
||||
}
|
||||
// lookup for filtering out chosen tags
|
||||
chosenTags[currentTags[i].toLowerCase()] = true;
|
||||
}
|
||||
|
||||
if (currentTag.tag) {
|
||||
// collect potential tags
|
||||
if (settings.url) {
|
||||
$.ajax({
|
||||
'url' : settings.url,
|
||||
'dataType' : 'json',
|
||||
'data' : { 'tag' : currentTag.tag },
|
||||
'async' : false, // wait until this is ajax hit is complete before continue
|
||||
'success' : function (m) {
|
||||
matches = m;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (i = 0; i < userTags.length; i++) {
|
||||
if (userTags[i].indexOf(currentTag.tag) === 0) {
|
||||
matches.push(userTags[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches = $.grep(matches, function (v, i) {
|
||||
return !chosenTags[v.toLowerCase()];
|
||||
});
|
||||
|
||||
if (settings.sort) {
|
||||
matches = matches.sort();
|
||||
}
|
||||
|
||||
for (i = 0; i < matches.length; i++) {
|
||||
html += '<' + settings.tagWrap + ' class="_tag_suggestion">' + matches[i] + '</' + settings.tagWrap + '>';
|
||||
}
|
||||
|
||||
tagMatches.html(html);
|
||||
suggestionsShow = !!(matches.length);
|
||||
} else {
|
||||
hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
function hideSuggestions() {
|
||||
tagMatches.empty();
|
||||
matches = [];
|
||||
suggestionsShow = false;
|
||||
}
|
||||
|
||||
function setSelection() {
|
||||
var v = tagsElm.val();
|
||||
|
||||
// tweak for hintted elements
|
||||
// http://remysharp.com/2007/01/25/jquery-tutorial-text-box-hints/
|
||||
if (v == tagsElm.attr('title') && tagsElm.is('.hint')) v = '';
|
||||
|
||||
currentTags = v.split(settings.separator);
|
||||
hideSuggestions();
|
||||
}
|
||||
|
||||
function chooseTag(tag) {
|
||||
var i, index;
|
||||
for (i = 0; i < currentTags.length; i++) {
|
||||
if (currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == workingTags.length - 1) tag = tag + settings.separator;
|
||||
|
||||
workingTags[i] = tag;
|
||||
|
||||
tagsElm.val(workingTags.join(settings.separator));
|
||||
tagsElm.blur().focus();
|
||||
setSelection();
|
||||
}
|
||||
|
||||
function handleKeys(ev) {
|
||||
fromTab = false;
|
||||
var type = ev.type;
|
||||
var resetSelection = false;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case 37: // ignore cases (arrow keys)
|
||||
case 38:
|
||||
case 39:
|
||||
case 40: {
|
||||
hideSuggestions();
|
||||
return true;
|
||||
}
|
||||
case 224:
|
||||
case 17:
|
||||
case 16:
|
||||
case 18: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 8: {
|
||||
// delete - hide selections if we're empty
|
||||
if (this.value == '') {
|
||||
hideSuggestions();
|
||||
setSelection();
|
||||
return true;
|
||||
} else {
|
||||
type = 'keyup'; // allow drop through
|
||||
resetSelection = true;
|
||||
showSuggestionsDelayed(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 9: // return and tab
|
||||
case 13: {
|
||||
if (suggestionsShow) {
|
||||
// complete
|
||||
chooseTag(matches[0]);
|
||||
|
||||
fromTab = true;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case 27: {
|
||||
hideSuggestions();
|
||||
setSelection();
|
||||
return true;
|
||||
}
|
||||
case 32: {
|
||||
setSelection();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'keyup') {
|
||||
switch (ev.charCode) {
|
||||
case 9:
|
||||
case 13: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resetSelection) {
|
||||
setSelection();
|
||||
}
|
||||
showSuggestionsDelayed(this, ev.charCode);
|
||||
}
|
||||
}
|
||||
|
||||
tagsElm.after(tagMatches).keypress(handleKeys).keyup(handleKeys).blur(function () {
|
||||
if (fromTab == true || suggestionsShow) { // tweak to support tab selection for Opera & IE
|
||||
fromTab = false;
|
||||
tagsElm.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// replace with jQuery version
|
||||
tagMatches = $(tagMatches).click(function (ev) {
|
||||
if (ev.target.nodeName == settings.tagWrap.toUpperCase() && $(ev.target).is('._tag_suggestion')) {
|
||||
chooseTag(ev.target.innerHTML);
|
||||
}
|
||||
}).addClass(settings.matchClass);
|
||||
|
||||
// initialise
|
||||
setSelection();
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
130
tagfield/tests/TagFieldTest.php
Normal file
130
tagfield/tests/TagFieldTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Ingo Schommer, SilverStripe Ltd. (<firstname>@silverstripe.com)
|
||||
* @package testing
|
||||
*
|
||||
* @todo Test filtering and sorting
|
||||
*/
|
||||
class TagFieldTest extends FunctionalTest {
|
||||
|
||||
function testExistingObjectSaving() {
|
||||
|
||||
}
|
||||
|
||||
function testNewObjectSaving() {
|
||||
|
||||
}
|
||||
|
||||
function testTextbasedSaving() {
|
||||
|
||||
}
|
||||
|
||||
function testObjectSuggest() {
|
||||
// partial
|
||||
$response = $this->post('TagFieldTestController/ObjectTestForm/fields/Tags/suggest', array('tag','tag'));
|
||||
$this->assertEquals($response->getBody(), '["tag1","tag2"]');
|
||||
|
||||
// full
|
||||
$response = $this->post('TagFieldTestController/ObjectTestForm/fields/Tags/suggest', array('tag','tag1'));
|
||||
$this->assertEquals($response->getBody(), '["tag1"]');
|
||||
|
||||
// case insensitive
|
||||
$response = $this->post('TagFieldTestController/ObjectTestForm/fields/Tags/suggest', array('tag','TAG1'));
|
||||
$this->assertEquals($response->getBody(), '["tag1"]');
|
||||
|
||||
// no match
|
||||
$response = $this->post('TagFieldTestController/ObjectTestForm/fields/Tags/suggest', array('tag','unknown'));
|
||||
$this->assertEquals($response->getBody(), '[]');
|
||||
}
|
||||
|
||||
function testTextbasedSuggest() {
|
||||
// partial
|
||||
$response = $this->post('TagFieldTestController/TextbasedTestForm/fields/Tags/suggest', array('tag','tag'));
|
||||
$this->assertEquals($response->getBody(), '["tag1","tag2"]');
|
||||
|
||||
// full
|
||||
$response = $this->post('TagFieldTestController/TextbasedTestForm/fields/Tags/suggest', array('tag','tag1'));
|
||||
$this->assertEquals($response->getBody(), '["tag1"]');
|
||||
|
||||
// case insensitive
|
||||
$response = $this->post('TagFieldTestController/TextbasedTestForm/fields/Tags/suggest', array('tag','TAG1'));
|
||||
$this->assertEquals($response->getBody(), '["tag1"]');
|
||||
|
||||
// no match
|
||||
$response = $this->post('TagFieldTestController/TextbasedTestForm/fields/Tags/suggest', array('tag','unknown'));
|
||||
$this->assertEquals($response->getBody(), '[]');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TagFieldTest_Tag extends DataObject implements TestOnly {
|
||||
|
||||
static $db = array(
|
||||
'Title' => 'Varchar(200)'
|
||||
);
|
||||
|
||||
static $belongs_many_many = array(
|
||||
'BlogEntries' => 'TagFieldTest_BlogEntry'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class TagFieldTest_BlogEntry extends DataObject implements TestOnly {
|
||||
|
||||
static $db = array(
|
||||
'Title' => 'Text',
|
||||
'Content' => 'Text',
|
||||
'TextbasedTags' => 'Text'
|
||||
);
|
||||
|
||||
static $many_many = array(
|
||||
'Tags' => 'TagFieldTest_Tag'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class TagFieldTest_Controller extends Controller {
|
||||
|
||||
static $url_handlers = array(
|
||||
// The double-slash is need here to ensure that
|
||||
'$Action//$ID/$OtherID' => "handleAction",
|
||||
);
|
||||
|
||||
public function ObjectTestForm() {
|
||||
$fields = new FieldSet(
|
||||
$tagField = new TagField('Tags', null, null, 'TagFieldTest_BlogEntry')
|
||||
);
|
||||
$actions = new FieldSet(
|
||||
new FormAction('ObjectTestForm_submit')
|
||||
);
|
||||
$form = new Form($this, 'ObjectTestForm', $fields, $actions);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function ObjectTestForm_submit($data, $form) {
|
||||
$data->saveInto($form);
|
||||
}
|
||||
|
||||
public function TextbasedTestForm() {
|
||||
$fields = new FieldSet(
|
||||
$tagField = new TagField('TextbasedTags', null, null, 'TagFieldTest_BlogEntry')
|
||||
);
|
||||
$actions = new FieldSet(
|
||||
new FormAction('TextbasedTestForm_submit')
|
||||
);
|
||||
$form = new Form($this, 'TextbasedTestForm', $fields, $actions);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function TextbasedTestForm_submit($data, $form) {
|
||||
$data->saveInto($form);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Director::addRules(50, array(
|
||||
'TagFieldTestController' => "TagFieldTest_Controller",
|
||||
));
|
||||
?>
|
20
tagfield/tests/TagFieldTest.yml
Normal file
20
tagfield/tests/TagFieldTest.yml
Normal file
@ -0,0 +1,20 @@
|
||||
TagFieldTest_Tag:
|
||||
tag1:
|
||||
Title: tag1
|
||||
tag2:
|
||||
Title: tag2
|
||||
TagFieldTest_BlogEntry:
|
||||
blogentry1:
|
||||
Title: blogentry1
|
||||
Tags: => TagFieldTest_Tag.tag1,TagFieldTest_Tag.tag2
|
||||
blogentry2:
|
||||
Title: blogentry2
|
||||
Tags: => TagFieldTest_Tag.tag1
|
||||
blogentry3:
|
||||
Title: blogentry3
|
||||
blogentry4:
|
||||
Title: blogentry4
|
||||
TextbasedTags: textbasedtag1 textbasedtag2
|
||||
blogentry5:
|
||||
Title: blogentry5
|
||||
TextbasedTags: textbasedtag1
|
Loading…
Reference in New Issue
Block a user