I'm trying to use the multi-select form element in a Moodle database to generate a list of tags. I would like these tags to link to the relevant search page displaying the filtered results.
The following template code works for singly tagged items, but fails for items with multiple tags:
<a href='/view.php?mode=list&filter=[[Tags]]'>[[Tags]]</a>
Is there a way to loop over items in a multi-select? Something like:
[[for Tag in Tags]] <a href='/view.php?mode=list&filter=[[Tag]]'>[[Tag]]</a> [[/for]]
I'm not certain there is an easy way to do this using the method above. Though, I've hacked together some javascript to accomplish the same thing:
function init() {
var tags = document.getElementsByClassName('tags');
for (var i=0; i<tags.length; i++) {
tags[i].innerHTML = tags[i].innerHTML.replace(/\w[\w\s]+?(?=<br>)/g, function(n) {
return "<a href='view.php?d=16&mode=list&perpage=10&filter=1&f_81%5B%5D="+ escape(n) + "'>" + n + "</a>";
});
}
};
window.onload = init;
Assuming you have an edit_form.php with something like this
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir . '/formslib.php');
class edit_form extends moodleform {
public function definition() {
$mform =& $this->_form;
$options = array('red' => 'red', 'blue' => 'blue', 'green' => 'green');
$select = $mform->addElement('select', 'tags', get_string('tags'), $options);
$select->setMultiple(true);
$this->add_action_buttons(false, get_string('submit'));
}
}
Then use this in your edit.php file
require_once(dirname(__FILE__) . '/edit_form.php');
...
$mform = new edit_form();
$mform->display();
if ($formdata = $mform->get_data()) {
foreach ($formdata->tags as $tag) {
$url = new moodle_url('/view.php', array('mode' => 'list', 'tag' => $tag));
echo html_writer::link($url, $tag);
}
}
Related
I am new Moodle and working on form data saving. I have created a group of checkbox which will take some input from the users and store it to the database, but unfortunately it is storing only 0/1 in the database. I have no clue where I made the mistake.
This my php file where I added the form field-
class custom_signup_form extends moodleform {
//Add elements to form
public function definition() {
global $CFG;
global $DB;
$mform = $this->_form; // Don't forget the underscore!
//$mform->addElement('text', 'email', get_string('email')); // Add elements to your form.
//$mform->setType('email', PARAM_NOTAGS); // Set type of element.
//$mform->setDefault('email', 'Please enter email'); // Default value.
if($this->content !== Null){
return $this->content;
}
$courses = $DB->get_records('course');
$categories = $DB->get_records('course_categories');
$interestedcourse=array();
foreach($courses as $course){
$coursestring = $course->fullname;
$interestedcourse[] = $mform->createElement('advcheckbox', 'interestedcourse[]','', $coursestring, array('group' => 1), array('',$coursestring));
}
$mform->addGroup($interestedcourse, 'interestedcoursegroup', get_string('interestedcourse', 'assignsubmission_metadata'),array('<br>'), false);
$interestedcategory=array();
foreach($categories as $category){
$categorystring = $category->name;
$interestedcategory[] = $mform->createElement('advcheckbox', 'interestedcategory[]','', $categorystring, array('group' => 1), array('',$categorystring));
}
$mform->addGroup($interestedcategory, 'interestedcategorygroup', get_string('interestedcategory', 'assignsubmission_metadata'),array('<br>'), false);
$this->add_action_buttons();
This is the php file for saving form data-
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/local/custom_signup/classes/form/edit.php');
global $DB;
$PAGE->set_url(new moodle_url('/local/custom_signup/signupform.php'));
$PAGE->set_title(get_string('custom_signup', 'local_custom_signup'));
#custom form for signup
$mform = new custom_signup_form();
if ($mform->is_cancelled()) {
// Go back to manage.php page
redirect($CFG->wwwroot . '/local/custom_signup/manage.php', get_string('cancelled_form', 'local_message'));
}
else if ($fromform = $mform->get_data()) {
var_dump($fromform);
die;
}
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->render_from_template('local_custom_signup/signupform', $templatecontext);
echo $OUTPUT->footer();
object returned from the form submission-
object(stdClass)#288 (3) { ["interestedcourse"]=> array(1) { [""]=> string(0) "" } ["interestedcategory"]=> array(1) { [""]=> string(0) "" } ["submitbutton"]=> string(12) "Save changes" }
I have an extbase extension with this TCA:
$GLOBALS['TCA']['tt_content']['types']['list']['previewRenderer']['items_item']
= \Vendor\Name\Backend\Preview\PreviewRenderer::class;
and this tsconfig:
mod.web_layout.tt_content.preview.list.items_item = EXT:EXT/Resources/Private/Templates/Preview/Items.html
Everything works well if i write the complete Code in this template file. But if i change it and use Partials like this:
<f:render arguments="{_all}" partial="Preview/List"/>
I only get the following error:
Error while rendering FluidTemplate preview using /typo3conf/ext/EXT/Resources/Private/Templates/Preview/Items.html The Fluid template files "" could not be loaded.
How can i set the Partial and Layout path?
Ive tried ist with module via typoscript but it doesnt work.
It depends on what your PreviewRender is doing.
The StandardContentPreviewRenderer supports only a template-file, no layouts, no partials. It's using a FluidStandaloneView and only setting setTemplatePathAndFilename() without setting paths for other parts of the template (renderContentElementPreviewFromFluidTemplate()):
protected function renderContentElementPreviewFromFluidTemplate(array $row): ?string
{
$tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
$fluidTemplateFile = '';
if ($row['CType'] === 'list' && !empty($row['list_type'])
&& !empty($tsConfig['list.'][$row['list_type']])
) {
$fluidTemplateFile = $tsConfig['list.'][$row['list_type']];
} elseif (!empty($tsConfig[$row['CType']])) {
$fluidTemplateFile = $tsConfig[$row['CType']];
}
if ($fluidTemplateFile) {
$fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
if ($fluidTemplateFile) {
try {
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename($fluidTemplateFile);
$view->assignMultiple($row);
if (!empty($row['pi_flexform'])) {
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
}
return $view->render();
} catch (\Exception $e) {
$this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [
'uid' => $row['uid'],
'file' => $fluidTemplateFile,
'exception' => $e,
]);
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->assign('error', [
'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()),
'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFile),
]);
$view->setTemplateSource('<f:be.infobox title="{error.title}" state="2">{error.message}</f:be.infobox>');
return $view->render();
}
}
}
}
return null;
}
If you want more advanced features in your preview, you have to extend/override this method.
Update:
StandaloneView extends TYPO3\CMS\Fluid\View\AbstractTemplateView, so you have also those setters:
setTemplate($templateName)
setTemplatePathAndFilename($templatePathAndFilename)
setTemplateRootPaths(array $templateRootPaths)
setPartialRootPaths(array $partialRootPaths)
setLayoutRootPaths(array $layoutRootPaths)
So setting paths for partials in your renderContentElementPreviewFromFluidTemplate() should solve it:
$view->setPartialRootPaths([
\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName('EXT:extensionfolder/Resources/Private/Partials'),
]);
I use an ActiveForm in Yii2 for my SearchModel. After click on search button the form fields remember previous values but SorterDropdown is refreshed.
<?php echo SorterDropdown::widget(['sort' => $dataProvider->sort,
'label' => $model->sortedBy($dataProvider->sort->attributes),])
?>
SorterDropdown is just a wrapper of ButtonDropdown.
How can I forse the SorterDropdown to remember sort order (and show it) after the form submition?
class SorterDropdown extends LinkSorter
{
public $label;
protected function renderSortLinks()
{
$attributes = empty($this->attributes) ? array_keys($this->sort->attributes) : $this->attributes;
$links = [];
foreach ($attributes as $name) {
$links[] = Html::tag('li', $this->sort->link($name, ['tabindex' => '-1']));
}
if (empty($this->label))
$this->label = 'Sort';
return \yii\bootstrap\ButtonDropdown::widget([
'encodeLabel' => false,
'label' => $this->label,
'dropdown' => [
'items' => $links,
],
]);
}
You should add some class to li element, which indicate active state of it and special style in CSS file.
I am a Yii Beginner and I am currently working on a Tagging system where I have 3 tables:
Issue (id,content,create_d,...etc)
Tag (id,tag)
Issue_tag_map (id,tag_id_fk,issue_id_fk)
In my /Views/Issue/_form I have added a MultiComplete Extension to retrieve multiple tag ids and labels,
I have used an afterSave function in order to directly store the Issue_id and the autocompleted Tag_ids in the Issue_tag_map table, where it is a HAS_MANY relation.
Unfortunately Nothing is being returned.
I wondered if there might be a way to store the autocompleted Tag_ids in a temporary attribute and then pass it to the model's afterSave() function.
I have been searching for a while, and this has been driving me crazy because I feel I have missed a very simple step!
Any Help or advices of any kind are deeply appreciated!
MultiComplete in Views/Issue/_form:
<?php
echo $form->labelEx($model, 'Tag');
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => '', //Was thinking of creating a temporary here
'name' => 'tag_autocomplete',
'splitter' => ',',
'sourceUrl' => $this->createUrl('Issue/tagAutoComplete'),
// Controller/Action path for action we created in step 4.
// additional javascript options for the autocomplete plugin
'options' => array(
'minLength' => '2',
),
'htmlOptions' => array(
'style' => 'height:20px;',
),
));
echo $form->error($model, 'issue_comment_id_fk');
?>
AfterSave in /model/Issue:
protected function afterSave() {
parent::afterSave();
$issue_id = Yii::app()->db->getLastInsertID();
$tag; //here I would explode the attribute retrieved by the view form
// an SQL with two placeholders ":issue_id" and ":tag_id"
if (is_array($tag))
foreach ($tag as $tag_id) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
// replace the placeholder ":issue_id" with the actual issue value
$command->bindValue(":issue_id", $issue_id, PDO::PARAM_STR);
// replace the placeholder ":tag_id" with the actual tag_id value
$command->bindValue(":tag_id", $tag_id, PDO::PARAM_STR);
$command->execute();
}
}
And this is the Auto Complete sourceUrl in the Issue model for populating the tags:
public static function tagAutoComplete($name = '') {
$sql = 'SELECT id ,tag AS label FROM tag WHERE tag LIKE :tag';
$name = $name . '%';
return Yii::app()->db->createCommand($sql)->queryAll(true, array(':tag' => $name));
actionTagAutoComplete in /controllers/IssueController:
// This function will echo a JSON object
// of this format:
// [{id:id, name: 'name'}]
function actionTagAutocomplete() {
$term = trim($_GET['term']);
if ($term != '') {
$tags = issue::tagAutoComplete($term);
echo CJSON::encode($tags);
Yii::app()->end();
}
}
EDIT
Widget in form:
<div class="row" id="checks" >
<?php
echo $form->labelEx($model, 'company',array('title'=>'File Company Distrubution; Companies can be edited by Admins'));
?>
<?php
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => 'company',
'splitter' => ',',
'name' => 'company_autocomplete',
'sourceUrl' => $this->createUrl('becomEn/CompanyAutocomplete'),
'options' => array(
'minLength' => '1',
),
'htmlOptions' => array(
'style' => 'height:20px;',
'size' => '45',
),
));
echo $form->error($model, 'company');
?>
</div>
Update function:
$model = $this->loadModel($id);
.....
if (isset($_POST['News'])) {
$model->attributes = $_POST['News'];
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
......
......
getRecordsFromAutocompleteString():
public static cordsFromAutocompleteString($string) {
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
$criteria = new CDbCriteria();
$criteria->select = 'id';
$criteria->condition = 'company =:company';
$companies = array();
foreach ($stringArray as $company) {
$criteria->params = array(':company' => $company);
$companies[] = Company::model()->find($criteria);
}
return $companies;
}
UPDATE
since the "value" porperty is not implemented properly in this extension I referred to extending this function to the model:
public function afterFind() {
//tag is the attribute used in form
$this->tag = $this->getAllTagNames();
parent::afterFind();
}
You should have a relation between Issue and Tags defined in both Issue and Tag models ( should be a many_many relation).
So in IssueController when you send the data to create or update the model Issue, you'll get the related tags (in my case I get a string like 'bug, problem, ...').
Then you need to parse this string in your controller, get the corresponding models and assigned them to the related tags.
Here's a generic example:
//In the controller's method where you add/update the record
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
Here the method I'm calling:
//parse your string ang fetch the related models
public static function getRecordsFromAutocompleteString($string, $model, $field)
{
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
return CActiveRecord::model($model)->findAllByAttributes(array($field => $stringArray));
}
So now your $issue->tags is an array containing all the related Tags object.
In your afterSave method you'll be able to do:
protected function afterSave() {
parent::afterSave();
//$issue_id = Yii::app()->db->getLastInsertID(); Don't need it, yii is already doing it
foreach ($this->tags as $tag) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
$command->bindValue(":issue_id", $this->id, PDO::PARAM_INT);
$command->bindValue(":tag_id", $tag->id, PDO::PARAM_INT);
$command->execute();
}
}
Now the above code is a basic solution. I encourage you to use activerecord-relation-behavior's extension to save the related model.
Using this extension you won't have to define anything in the afterSave method, you'll simply have to do:
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
$issue->save(); // all the related models are saved by the extension, no afterSave defined!
Then you can optimize the script by fetching the id along with the tag in your autocomplete and store the selected id's in a Json array. This way you won't have to perform the sql query getRecordsFromAutocompleteString to obtain the ids. With the extension mentioned above you'll be able to do:
$issue->tags = CJSON::Decode($_POST['idTags']);//will obtain array(1, 13, ...)
$issue->save(); // all the related models are saved by the extension, the extension is handling both models and array for the relation!
Edit:
If you want to fill the autocomplete field you could define the following function:
public static function appendModelstoString($models, $fieldName)
{
$list = array();
foreach($models as $model)
{
$list[] = $model->$fieldName;
}
return implode(', ', $list);
}
You give the name of the field (in your case tag) and the list of related models and it will generate the appropriate string. Then you pass the string to the view and put it as the default value of your autocomplete field.
Answer to your edit:
In your controller you say that the companies of this model are the one that you added from the Autocomplete form:
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
So if the related company is not in the form it won't be saved as a related model.
You have 2 solutions:
Each time you put the already existing related model in you autocomplete field in the form before displaying it so they will be saved again as a related model and it won't disapear from the related models
$this->widget('application.extensions.multicomplete.MultiComplete', array(
'name' => 'people',
'value' => (isset($people))?$people:'',
'sourceUrl' => array('searchAutocompletePeople'),
));
In your controller before calling the getRecordsFromAutocompleteString you add the already existing models of the model.
$model->companies = array_merge(
$model->companies,
$this->getRecordsFromAutocompleteString($_POST['News']['company'])
);
I was wondering if is there an easy way to generate views from form objects when dealing with CRUDs.
I mean, when we have these options: VIEW | EDIT | DELETE
I want my VIEW option like EDIT option, but without form elements, just the values.
This will minimize so much the time spent to create these views.
Someone knowks something like that?
In my last project I had this dilemma too. My solution may not be the most elegant, but it did the job. Mind you, I use a Form viewscript decorator in stead of a full decorator generated elements. But you could adjust this example to use decorators I presume. What I'm showing is a very basic example, to give you a general idea. Here's what I did:
class Cms_Form_Page extends Zend_Form
{
const FOR_CREATE = 'forCreate';
const FOR_READ = 'forRead';
const FOR_UPDATE = 'forUpdate';
const FOR_DELETE = 'forDelete';
protected $_name = 'page';
private $_for;
private $_viewScripts = array(
self::FOR_CREATE => 'page-manager/partials/form-page-create.phtml',
self::FOR_READ => 'page-manager/partials/form-page-read.phtml',
self::FOR_UPDATE => 'page-manager/partials/form-page-update.phtml',
self::FOR_DELETE => 'page-manager/partials/form-page-delete.phtml'
);
public function __construct( $for = self::FOR_CREATE, $options = null )
{
$this->_for = $for;
parent::__construct( $options );
}
public function init()
{
$this->setName( $this->_name )
->setAttribs( array( 'accept-charset' => 'utf-8' ) )
->setDecorators( array(
'PrepareElements',
array( 'ViewScript', array( 'viewScript' => $this->_viewScripts[ $this->_for ] ) ),
'Form'
) );
$elements = array();
swith( $this->_for )
{
case self::FOR_CREATE:
$title = new Zend_Form_Element_Text( 'title' );
$elements[] = $title;
break
case self::FOR_READ:
$id = new Zend_Form_Element_Hidden( 'id' );
$elements[] = $id;
break;
case self::FOR_UPDATE:
$id = new Zend_Form_Element_Hidden( 'id' );
$elements[] = $id;
$title = new Zend_Form_Element_Text( 'title' );
$elements[] = $title;
break;
case self::FOR_DELETE:
$id = new Zend_Form_Element_Hidden( 'id' );
$elements[] = $id;
break;
default:
throw new Exception( 'invalid Form type' );
}
$submit = new Zend_Form_Element_Button( 'submit' );
$elements[] = $submit;
$this->addElements( $elements );
}
}
So, basically, I pass one of the class constants to it's constructor. And based on that value, I determine what elements are needed for the form, and how the elements should be rendered.
For instance, for create you could have a select dropdown form field where you would choose a Locale, where for delete this would be a hidden field (not shown in my example btw).
Hope this has given you some ideas.
PS:
In one of the selected viewscripts you could then simply show the value of an element (along with rendering the hidden element too), with something like:
<?
$form = $this->element;
?>
... some html
// let's presume id and locale are hidden form fields for current form type
// (Cms_Form_Page::FOR_UPDATE for instance)
<?= $form->id->renderViewHelper(); ?>
<?= $form->locale->renderViewHelper(); ?>
// and here we simply output the current locale value
// of course, you should have populated the values in the form somewhere first
<dt>Current locale:</dt>
<dd><?= $form->locale->getValue(); ?></dd>
...etc
So, I think you'ld be best of with using viewscript decorators for the form, or you could roll your own form element decorator that renders the hidden field (if neccesary) and simply shows it's value in some html tag.
Hector from Nabble, show me this, which seems to be the best way:
class Default_View_Helper_FormView extends Zend_View_Helper_Abstract
{
public function formView(Zend_Form $form)
{
$html = "<dl>";
foreach ($form->getElements as $element) {
$html .= "<dt>{$element->getLabel()}</dt>";
$html .= "<dd>{$element->getValue()}</dd>";
}
$html .= "</dl>";
return $html;
}
}
I'm not sure if I understand, but I think that for the view option you can just fetch the data from your model. No need to access them through Zend_Form.
But if you want the make your form read-only, you can add readonly (setAttrib('readonly', 'readonly')) attribute to your elements.
Made a couple minor additions to the accepted answer to cover common elements that may be special cases:
class Default_View_Helper_FormView extends Zend_View_Helper_Abstract
{
public function formView( Zend_Form $form )
{
$html = '<dl>';
foreach ( $form->getElements() as $element ) {
if( $element instanceof Zend_Form_Element_Submit ) {
continue;
}
$html .= '<dt>' . $element->getLabel() . '</dt>';
$value = $element->getValue();
if( $element instanceof Zend_Form_Element_Checkbox ) {
$value = ($value) ? 'Yes' : 'No';
}
else if( $element instanceof Zend_Form_Element_Select ) {
$value = $element->getMultiOption($value);
}
$html .= '<dd>' . $value . '</dd>';
}
$html .= '</dl>';
return $html;
}
}
The only problem with the accepted answer is that you're creating all the elements and then ignoring them.
Using the control logic from fireeyedboy's answer, you could instead switch all the elements to Zend_View_Helper_FormNote which does the same thing.
Just depends on if the optimization matters.