I have a view for an project. When I do an Ajax call on the method showProjects() in my ProjectController, I would get a json feed which contain an array of templates for each item.
Actually, I try that, but I get a json feed which contain request objects , not the templates. I don't want to display one template with each item, because I want to process them in js.
#ProjectController.php
public function showProjects() {
$em = $this->getDoctrine()->getEntityManager();
$projects = $em->getRepository('BtaskBoardBundle:Project')->findAll();
if (!$projects) {
throw new NotFoundHttpException();
}
$projects_template = array();
foreach ($projects as $project) {
$projects_template[] = $this->render('MyBundle::project.html.twig', array(
'project' => $project,
));
}
return new Response(json_encode($projects_template), 200);
}
#project.html.twig
<a class="project" data-id="{{ project.id }}" href="#">{{ project.name }}</a>
What's wrong?
Thanks in advance.
The response object you get back should have a getContent() method. Check out the Symfony\Component\HttpFoundation\Response class in the documentation for more information. You should be able to do it like so:
foreach ($projects as $project) {
$projects_template[] = $this->render('MyBundle::project.html.twig', array(
'project' => $project,
))->getContent();
}
Related
I'm trying to do something very different for a SilverStripe site: on several subpages are tables of data, and these tables each have their own set of column headers, and some tables have more columns than others. I want to avoid building out tables in the Rich Text Editor as that is prone to a lot of mistakes and it's a hassle to maintain over time.
What I would like to do is create a DataObject that allows for a nth number of columns and an nth number of corresponding rows. This way I can call a loop (or possibly two) inside the template where I have the HTML table structure already in place. The content managers have full control over which columns are in the tables for any give subpage, and they don't have to worry about maintaining the HTML table setup.
I've had a couple of ideas that don't produce the results I want without a) making the UI experience too complex for content managers and b) not being able to properly link the columns with the rows.
I have thought of creating a DataObject for Table Headers and one for Table Rows, but then I'm stumped on how to combine them in such a way that would make sense, especially since there could be any number of columns.
Would anyone have any suggestions on to approach this?
UPDATE: Ok, I have something going for the TableRowItem data object that may work, and is close to working. However, the issue is this now: How do I save the field values to the database when I am creating them basically on the fly? As it is now, the only field that saves to the database is the PDF file upload field, everything else is erased upon hitting "create."
<?php
class TruckBodyPdfTableRowItem extends DataObject {
private static $db = array(
);
// One-to-one relationship with gallery page
private static $has_one = array(
'TablePage'=> 'Page',
'TableColumnSet' => 'TableColumnSet',
'PDF' => 'File',
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","TablePageID");
$fields->removeFieldFromTab("Root.Main","TableColumnSetID");
$fields->removeFieldFromTab("Root.Main","SortOrder");
$fields->addFieldsToTab("Root.Main", $this->getMyColumnOptions());
return $fields;
}
public function getMyColumnOptions()
{
$columnArray = [];
$Columns = DataObject::get('TableColumnSet');
foreach($Columns as $Column){
$columnArray[] = TextField::create($Column->TableColumnHeader);
}
return $columnArray;
}
// Tell the datagrid what fields to show in the table
private static $summary_fields = array(
);
public function canEdit() {
return true;
}
public function canDelete() {
return true;
}
public function canCreate(){
return true;
}
public function canPublish(){
return true;
}
public function canView(){
return true;
}
}
But those are the tricky parts: Figuring out how to map values from one DataObject into labels for another, and then auto-generating an nth number of rows based on how many columns have been created.
<?php
class TablePage extends Page
{
private static $db = array(
'H1' => 'varchar(250)',
);
private static $has_many = array(
'TableRowItems' => 'TableRowItem',
'TableColumnSets' => 'TableColumnSet'
);
private static $has_one = array(
);
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldToTab("Root.Main", new TextField("H1"), "Content");
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
// field from drawer class => label in UI
'TableColumnHeader' => 'Table Column Header'
));
$gridfield = new GridField(
"TableColumnSets",
"Table Column Sets",
$this->TableColumnSets(),
$gridFieldConfig
);
$fields->addFieldToTab('Root.Specs Table', $gridfield);
$gridFieldConfig2 = GridFieldConfig_RecordEditor::create();
$gridFieldConfig2->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
// field from drawer class => label in UI
'TableRowValue' => 'Table Row Value'
));
$gridfield2 = new GridField(
"TableRowItems",
"Table Row Items",
$this->TableRowItems(),
$gridFieldConfig2
);
$fields->addFieldToTab('Root.Specs Table', $gridfield2);
return $fields;
}
}
class TablePage_Controller extends Page_Controller
{
private static $allowed_actions = array(
);
public function init()
{
parent::init();
// You can include any CSS or JS required by your project here.
// See: http://doc.silverstripe.org/framework/en/reference/requirements
}
}
Here are the classes TableColumnSet and TableRowValue. I figured, there would be one set of column headers associated with an nth number of rows, so I figured there would be a $has_many relationship between the two classes, in that a TableColumnSet could have many TableRowValues, but there would only be one TableColumnSet for all the TableRowValues. I was hoping to associate the TableRowValues to the TableColumnSet values using a dropdown with all the column headers created but that just sounds like a bad idea. Having to manually associate every field in a row to the column headers seems tedious and potentially difficult content managers.
<?php
class TableColumnSet extends DataObject {
private static $db = array(
'SortOrder' => 'Int',
'TableColumnHeader'=>'varchar(250)'
);
// One-to-one relationship with gallery page
private static $has_one = array(
'TablePage'=> 'Page'
);
private static $has_many = array(
'TableRowItems' => 'TableRowItem'
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","TablePageID");
$fields->removeFieldFromTab("Root.Main","SortOrder");
return $fields;
}
// Tell the datagrid what fields to show in the table
private static $summary_fields = array(
'TableColumnHeader' => 'Table Column Header',
);
public function canEdit() {
return true;
}
public function canDelete() {
return true;
}
public function canCreate(){
return true;
}
public function canPublish(){
return true;
}
public function canView(){
return true;
}
}
I feel like may be on to something here, at least in regards to the relationship between the column headers and rows? I'm not sure, though.
I might be off base here, since I have no experience with SilverStripe. But... my PHP / HTML table solution might apply here:
<?php
// parse your table data into this structure
$tableData = array(
"rowOne" => array(
"columnName" => "columnValue1",
"colName" => "colValue1"
// .....
),
"rowTwo" => array(
"columnName" => "columnValue2",
"colName" => "colValue2"
// .....
)
);
// now loop through the array with a printHeader parameter
$tableHTML = array(
"<table>"
);
$tableHead = array(
"<thead>"
);
$tableBody = array(
"<tbody>"
);
$printHeader = true;
foreach ($tableData as $row) {
foreach ($row as $column => $value) {
$tableRow = "<tr>";
if ($printHeader) {
$tableHead[] = "<th>".$column."</th>";
}
$tableRow .= "<td>".$value."</td>";
}
$tableBody[] = $tableRow."</tr>";
// after the first row, set printHeader to false and close the <thead>
$printHeader = false;
$tableHead[] = "</thead>";
}
// implode table header to string with linebreaks
$tableHead = implode(PHP_EOL, $tableHead);
// close table <tbody> & implode to string with linebreaks
$tableBody[] = "</tbody>";
$tableBody = implode(PHP_EOL, $tableBody);
// add all table elements together
$tableHTML[] = $tableHead;
$tableHTML[] = $tableBody;
$tableHTML[] = "</table>";
// implode table array to string
$tableHTML = implode(PHP_EOL, $tableHTML);
// print or write anywhere
echo($tableHTML);
?>
The array structure for all the steps in the loop are to keep the default server memory cleaner to remove old data. If you concat ($var .= "string";) everything as strings all the references will stay stored in memory and bog down the server when displaying large tables.
I hope this is of some help
I'm searching for a way to create a custom action button which allows me to make a new DataObject with pre-filled content from another DataObject. As a simple example: When I have an email and click the "answer"-button in my email-client, I get a new window with pre-filled content from the email before. I need exactly this functionality for my button. This button should appear next to each DataObject in the GridField.
So I know how to make a button and add it to my GridField (--> https://docs.silverstripe.org/en/3.2/developer_guides/forms/how_tos/create_a_gridfield_actionprovider/) and I know how to go to a new DataObject:
Controller::curr()->redirect($gridField->Link('item/new'));
I also found out that there is a duplicate function for DataObjects:
public function duplicate($doWrite = true) {
$className = $this->class;
$clone = new $className( $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
if($doWrite) {
$clone->write();
$this->duplicateManyManyRelations($this, $clone);
}
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
return $clone;
}
Perhaps it's easier than I think but at the moment I just don't get how to rewrite this to get what I need. Can somebody give me a hint?
That's for sure not the cleanest solution but I think it should do the trick.
At first let's create the custom gridfield action. Here we will save all accessible records in a session and add a query string to the url so that we'll know which object we want to "clone"
public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'clone'.$record->ID,
'Clone',
'clone',
array('RecordID' => $record->ID)
);
$values = Session::get('ClonedData');
$data = $record->data()->toMap();
if($arr = $values) {
$arr[$record->ID] = $data;
} else {
$arr = array(
$record->ID => $data
);
}
Session::set('ClonedData', $arr);
return $field->Field();
}
public function getActions($gridField) {
return array('clone');
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'clone') {
$id = $arguments['RecordID'];
Controller::curr()->redirect($gridField->Link("item/new/?cloneID=$id"));
}
}
after adding this new component to our gridfield,
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
we'll need to bring the data into the new form. To do so, add this code directly above "return $fields" on your getCMSFields function so it will be executed every time we'll open this kind of object.
$values = Session::get('ClonedData');
if($values) {
Session::clear('ClonedData');
$json = json_encode($values);
$fields->push(LiteralField::create('ClonedData', "<div id='cloned-data' style='display:none;'>$json</div>"));
}
At the end we need to bring the content back into the fields. We'll do that with a little bit of javascript so at first you need to create a new script.js file and include it in the ss backend (or just use an existing one).
(function($) {
$('#cloned-data').entwine({
onmatch: function() {
var data = JSON.parse($(this).text()),
id = getParameterByName('cloneID');
if(id && data) {
var obj = data[id];
if(obj) {
$.each(obj, function(i, val) {
$('[name=' + i + ']').val(val);
});
}
}
}
});
// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript#answer-901144
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
})(jQuery);
And that's it ... quite tricky. Hope it will solve your problem.
I am attempting to add the 'AbsoluteLink' property to each DataObject in a DataList and then convert the list to JSON with JSONDataFormatter::convertDataObjectSet().
I have the following function:
public function json() {
$data = ResourceCentreArticlePage::get()->filter('ShowInMenus', '1')->filter('ShowInSearch', '1')->sort('Created', 'DESC');
$pageArray = new ArrayList();
foreach ($data as $page) {
$page->AbsoluteLink = $page->AbsoluteLink();
$pageArray->push($page);
}
// If I dump out the content of $pageArray here the object has the AbsoluteLink property
$jsonFormatter = new JSONDataFormatter();
$jsonData = $jsonFormatter->convertDataObjectSet($pageArray);
// If I dump out the content of $jsonData here there is no AbsoluteLink property
$this->response->addHeader("Content-type", "application/json");
return $jsonData;
}
The problem:
The AbsoluteLink property is removed after running the $pageArray through the convertDataObjectSet method.
What am I missing?
Using $jsonFormatter->setCustomAddFields(); will help here.
Add the following to the Page class:
public function getMyAbsoluteLink() {
return $this->AbsoluteLink();
}
For example to the Page.php:
class Page extends SiteTree {
public function getMyAbsoluteLink() {
return $this->AbsoluteLink();
}
}
And use that "magic field" like this:
public function json() {
$pages = Page::get()
->filter('ShowInMenus', '1')
->filter('ShowInSearch', '1')
->sort('Created', 'DESC');
$jsonFormatter = new JSONDataFormatter();
// add your custom field
$jsonFormatter->setCustomAddFields(["MyAbsoluteLink"]);
$jsonData = $jsonFormatter->convertDataObjectSet(
$pages
);
return $jsonData;
}
Note the $jsonFormatter->setCustomAddFields(["MyAbsoluteLink"]); and I removed the array manipulation.
Also I removed your array manipulation. How the convertDataobjectSet function works it seems you can't amend the objects before it runs.
I'm following this tutorial
I have a page called 'Contact.ss'. The php file looks like this:
class Contact extends Page {
private static $has_one = array (
'Photograph' => 'Image'
);
static $db = array (
'MailTo' => 'Varchar(100)',
'SubmitText' => 'Text'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main', $Photograph = UploadField::create('Photograph'), 'Content');
$Photograph->getValidator()->setAllowedExtensions(array('png','jpeg','jpg','gif'));
$Photograph->setFolderName('photographs');
$fields->addFieldToTab("Root.Main", new Textfield('Mailto', 'Address to email contact form submission to'));
$fields->addFieldToTab("Root.Main", new TextareaField('SubmiteText', 'Text displayed after contact form submission'));
return $fields;
}
}
class Contact_Controller extends Page_Controller {
static $allowed_actions = array(
'ContactForm'
);
function ContactForm() {
// Create fields
$fields = new FieldSet(
new TextField('Name', 'Name*'),
new EmailField('Email', 'Email*'),
new TextareaField('Comments','Comments*')
);
// Create action
$actions = new FieldSet(
new FormAction('SendContactForm', 'Send')
);
// Create Validators
$validator = new RequiredFields('Name', 'Email', 'Comments');
return new Form($this, 'ContactForm', $fields, $actions, $validator);
}
}
But when I call $ContactForm in the template I get a blank screen when I try to load the page. (500 error)
I've checked to see if it's possible to call the function from the template by replacing all the ContactForm()'s code with:
return "Hello, World!"
It works, so I know the function is being called. But I can't see what's wrong with the code from the tutorial.
Can anyone help me out?
The issue is the tutorial you have used is written for SilverStripe 2.4 while you are using a newer version, SilverStripe 3.1.
For SilverStripe 3.1 I suggest going through the SilverStripe Frontend Forms lesson rather than the SSBits tutorial. The SSBits tutorial is from 2010 and is for SilverStripe 2.4. The SilverStripe Frontend Forms lesson is from 2015 and is for the current version of SilverStripe.
With your current code there are a number of bits of code that need to be updated to work in the latest version of SilverStripe.
FieldSet has been replaced by FieldList. You will need to replace each instance of FieldSet with FieldList in your code.
Your ContactForm should look more like this:
function ContactForm() {
// Create fields
$fields = FieldList::create(
TextField::create('Name', 'Name*'),
EmailField::create('Email', 'Email*'),
TextareaField::create('Comments','Comments*')
);
// Create action
$actions = FieldList::create(
FormAction::create('SendContactForm', 'Send')
);
// Create Validators
$validator = RequiredFields::create('Name', 'Email', 'Comments');
return Form::create($this, 'ContactForm', $fields, $actions, $validator);
}
In SilverStripe 3.1 the in built static variables need to be declared private.
Make sure you declare your $allowed_actions as private:
private static $allowed_actions = array(
'ContactForm'
);
As well as your $db as private:
private static $db = array (
'MailTo' => 'Varchar(100)',
'SubmitText' => 'Text'
);
Currently I cant get subclasses to appear in a list view using sonta admin bundle for symfony 2
I can get it working for create forms as per the advanced config page (http://sonata-project.org/bundles/admin/2-1/doc/reference/advance.html) but how can you do this with the list view?
If i pass the subclass in the url - list?subclass=MySubClassName
and set the object in my listAction
$object = $this->admin->getNewInstance();
$this->admin->setSubject($object);
I can get the subject and configure the correct fields with configureListFields()
if ($subject instanceof MySubClassName) {
$listMapper->add('MySubClassNameID');
$listMapper->add('MySubClassNameKey');
$listMapper->add('MySubClassNameStatus','text');
}
but the end results table is always blank and the symfony debug toolbar seems to show that the db queries are looking for the parent class. Anyone got this to work?
I'm not sure what you mean with those "subclasses" in the list view, but if you want to add a field form another entity (connected through a foreign key with yours) you can do it lie this:
$listMapper
->addIdentifier('id')
->addIdentifier('title')
->add('name')
->add('entity1.customField1')
->add('entity2.customField2');
Incase anyone else faces this I found out how to do this.
To make it work in a way similar to the edit page you would pass the subclass in the url
...list?subclass=MySubClass
set the subject of your listAction in your custom admin crud controller
public function listAction()
{
if (false === $this->admin->isGranted('LIST')) {
throw new AccessDeniedException();
}
if ($listMode = $this->getRequest()->get('_list_mode')) {
$this->admin->setListMode($listMode);
}
$this->admin->setSubject($this->admin->getNewInstance());
$datagrid = $this->admin->getDatagrid();
$formView = $datagrid->getForm()->createView();
// set the theme for the current Admin Form
$this->get('twig')->getExtension('form')->renderer->setTheme($formView, $this->admin->getFilterTheme());
return $this->render($this->admin->getTemplate('list'), array(
'action' => 'list',
'form' => $formView,
'datagrid' => $datagrid,
'csrf_token' => $this->getCsrfToken('sonata.batch'),
));
}
and then over-ride the createQuery method in your Admin class
public function createQuery($context = 'list')
{
$cName = get_class($this->getSubject());
$query = $this->getModelManager()->createQuery($cName);
foreach ($this->extensions as $extension) {
$extension->configureQuery($this, $query, $context);
}
return $query;
}
If you pass anything with url parameters you should also override getPersistentParameters to add your url request to Pager, FilterForm and the form for batchActions (or others that appear on the list view)
<?php
class YourAdmin extends Admin
{
public function getPersistentParameters()
{
if (!$this->getRequest()) {
return array();
}
return array(
'subclass' => $this->getRequest()->get('subclass'),
);
}
}