Показаны сообщения с ярлыком admin. Показать все сообщения
Показаны сообщения с ярлыком admin. Показать все сообщения

вторник, 21 сентября 2010 г.

Symfony: backend pager



Что бы получше выделить текущую страницу в пейджере далаем небольшие изменения:

1. CSS
.currentPage {
border: 1px solid #ff9900;
padding: 4px;
background: #eeeeff;
}


2. layout.php

jQuery(document).ready(function($){

// try to find pager
pager = $('.sf_admin_pagination').html();
if (pager) {
var re = /(\s+)(\d+)(\s)+/;
var newstr = pager.replace(re, "$2");
$('.sf_admin_pagination').html(newstr);
}

среда, 8 апреля 2009 г.

Symfony 1.2 tabular(embedded) subforms

Довольно часто при редактировании требуется вывести N дочерних записей в виде таблицы.
К сожалению стандартными средствами Symfony этого сделать не получилось, в первую очередь из за того, что Symfony в каждой строке оставляет место для сообщений об ошибках валидации.
Постараемся с минимумом усилий сделать таблицу с подчиненными записями.
Для примера возьмем очень простую ситуацию - в нашем интернет-магазине каждый товар может иметь 0-N рекомендованных товаров.

Кусок схемы:

propel:
pc_product:
_attributes: { phpName: Product }
id: ~
name: varchar(255)
price1: float
price2: float
description: longvarchar
pic: varchar(255)
is_published: boolean
category_id: { type: integer, foreignReference: id, foreignTable: pc_category, onDelete: cascade, onUpdate: cascade, required: true }

pc_product_recommended:
_attributes: { phpName: ProductRecommended }
id: ~
product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
recommend_product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
note: { type: varchar(512), default: null }
pos: integer

pc_category:
_attributes: { phpName: Category, treeMode: NestedSet }
id: ~
name: varchar(255)
alias: varchar(1024)
lft: { type: integer, required: false, default: 0, nestedSetLeftKey: 'true'}
rgt: { type: integer, required: false, default: 0, nestedSetRightKey: 'true'}
scope: { type: integer, required: false, default: 0, treeScopeKey: 'true'}
parent: { type: integer }
is_published: { type: boolean }
deleted_at: { type: date }


Изменяем /lib/forms/ProductForm.class.php

class ProductForm extends BaseProductForm
{
public function configure()
{
$this->setWidgets(array(
'category_id' => new sfWidgetFormPropelChoice(array('model' => 'Category', 'add_empty' => false)),
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
'price1' => new sfWidgetFormInput(),
'price2' => new sfWidgetFormInput(),
'pic' => new myWidgetFormInputFileEditable(),
'is_published' => new sfWidgetFormInputCheckbox(),
));


$this->setValidators(array(
'id' => new sfValidatorPropelChoice(array('model' => 'Product', 'column' => 'id', 'required' => false)),
'name' => new sfValidatorString(array('max_length' => 255, 'required' => false)),
'price1' => new sfValidatorNumber(array('required' => false)),
'price2' => new sfValidatorNumber(array('required' => false)),
'is_published' => new sfValidatorBoolean(array('required' => false)),
'category_id' => new sfValidatorPropelChoice(array('model' => 'Category', 'column' => 'id')),
'pic' => new sfValidatorFile(array(
'required' => false,
'max_size' => '102400', // bytes (1MB)
'mime_types' => array('image/png', 'image/jpeg', 'image/gif',)
)),
));

$this->widgetSchema->setNameFormat('product[%s]');

$this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);

//получим дочерние записи
$c = new Criteria();
$c->add(ProductRecommendedPeer::PRODUCT_ID, $this->getObject()->getId());
$c->addAscendingOrderByColumn(ProductRecommendedPeer::ID);
$rps = ProductRecommendedPeer::doSelect($c);
//цикл по дочерним записям
foreach ($rps as $index => $rProduct)
{
$rProductForm = new ProductRecommendedForm($rProduct);
$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox());
$rProductForm->setValidator('del_me', new sfValidatorBoolean(array('required' => false)));
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('rProduct-'.$index, $rProductForm);
$this->widgetSchema->setLabel('rProduct-'.$index, $index+1);
}
$rProductForm = new ProductRecommendedForm();

$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
//добавляем дополнительный чекбокс, для удаления записи
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox(
array(),
array('disabled' => true)
));
if (!$this->getObject()->isNew())
{
//добавим пустой элемент в списке, на случай если мы не хотим добавлять новую запись
$rProductForm->setWidget('recommend_product_id', new sfWidgetFormPropelChoice(
array('model' => 'Product', 'add_empty' => true)));

$rProductForm->setDefaults(array('product_id' => $this->getObject()->getId()));
//отключим обязательный выбор значения листбокса
$rProductForm->setValidator('recommend_product_id', new sfValidatorInteger(array('required' => false)));
//переносим чекбокс для удаления записи наверх
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('new-rec', $rProductForm);
$this->widgetSchema->setLabel('new-rec', 'new record');
}


}

public function updateObject($values = null)
{
$object = parent::updateObject();
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.
sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$object->setPic(str_replace($path.'/', '', $object->getPic()));
return $object;
}
/*
* do save
*/
protected function doSave($con = null){
$values = $this->getValues();

$path = sfConfig::get('sf_root_dir').'/'.
sfConfig::get('sf_web_dir_name').'/'.sfConfig::get('sf_upload_dir');

if (isset($values['pic_delete'])){
$currentFile = $path.'/'.$this->getObject()->getPic();
if (is_file($currentFile)){
unlink($currentFile);
}
$this->getObject()->setPic('');
}

$file = $values['pic'];

if(!empty($file)){
if (file_exists(sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getPic())){
@unlink($this->getObject()->getPic());
}
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$file->save($path.'/'.$filename);
}

return parent::doSave($con);
}

}



Если в /generator.yml настройки форм по умолчанию:

form: ~
edit: ~
new: ~

мы видим внизу формы редактирования доп.форму с добавлением записи

она даже работает ;) но внешний вид нас не устраивает, и по работе тоже есть нарекания, при каждом обновлении родительской формы без указания нового рекомендованного товара, у нас добавляется пустая запись, а так же еще не работает удаление записей.

Давайте исправим внешний вид и логику добавления-удаления.

Редактируем в config/generator.yml настройки формы указываем поля для отображения:

form:
display: [ name, price1, price2, pic, is_published ]

Такая небольшая настройка приводит к тому, что исчезают наши встроенные формы, подробнее об этом явлении как нибудь позже...
При этом форма перестает работать, что неудивительно, т.к. мы к форме прикрепили массу валидаторов для подчиненных форм.
Теперь мы в ручную отобразим подформы в табличном виде.
Заходим в кэш и копируем
/cache/backend/.../autoProduct/templates/_form.php в
apps/backend/modules/product/templates/_form.php

Далее заменяем содежимое этого файла на:

<div class="sf_admin_form">
<?php echo form_tag_for($form, '@product') ?>
<?php echo $form->renderHiddenFields() ?>

<?php if ($form->hasGlobalErrors()): ?>
<?php echo $form->renderGlobalErrors() ?>
<?php endif; ?>

<?php foreach ($configuration->getFormFields($form, $form->isNew() ? 'new' : 'edit') as $fieldset => $fields): ?>
<?php include_partial('product/form_fieldset', array('product' => $product, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?>
<?php endforeach; ?>

<?php if ($form->getEmbeddedForms()): ?>
<fieldset id="sf_fieldset_recommended_products">
<h2>Recommended products</h2>
<table>
<tr>
<th>#</th>
<th>del</th>
<th>product</th>
<th>note</th>
<th>pos</th>
</tr>
<?php foreach ($form->getEmbeddedForms() as $index => $embForm): ?>
<tr>
<?php echo $form[$index]['id']->render() ?>
<?php echo $form[$index]['product_id']->render() ?>
<td><?php echo $form[$index]->renderLabel() ?></td>
<td><?php echo $form[$index]['del_me']->renderError() ?><?php echo $form[$index]['del_me']->render() ?></td>
<td><?php echo $form[$index]['recommend_product_id']->renderError() ?><?php echo $form[$index]['recommend_product_id']->render() ?></td>
<td><?php echo $form[$index]['note']->renderError() ?><?php echo $form[$index]['note']->render() ?></td>
<td><?php echo $form[$index]['pos']->renderError() ?><?php echo $form[$index]['pos']->render() ?></td>
</tr>
<?php endforeach; ?>
</table>
</fieldset>
<?php endif; ?>

<?php include_partial('product/form_actions', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>
</form>
</div>



Подформа преобрела более приятный вид, остается разобраться с обновлением и удалением.
Добавляем новый метод в /lib/forms/ProductForm.class.php

public function updateObjectEmbeddedForms($values, $forms = null)
{
if (is_null($forms))
{
$forms = $this->embeddedForms;
}

foreach ($forms as $name => $form)
{
if (!is_array($values[$name]))
{
continue;
}

if ($form instanceof sfFormPropel)
{
if (isSet($values[$name]['del_me'])){
if ($values[$name]['del_me'] == 1)
{
$form->getObject()->delete();
unset($this->embeddedForms[$name]);
continue;
}
} else {
if (strstr($name, 'new-'))
{
//именно тут мы сами выступаем в роли валидатора, и если одно из обязательных полей пустое,
//мы игнорируем новую запись
$rProduct = $values[$name]['recommend_product_id'];
$rPos = $values[$name]['pos'];
if ( empty($rProduct) OR empty($rPos))
{
unset($this->embeddedForms[$name]);
continue;
}
}
}//if !isset del me
$form->updateObject($values[$name]);
}
else
{
$this->updateObjectEmbeddedForms($values[$name], $form->getEmbeddedForms());
}
}
//die();
}//updateObjectEmbeddedForms

пятница, 14 ноября 2008 г.

Symfony 1.2: file upload in admin

Это черновик

/backend/modules/author/lib/form/myAuthorForm.class.php

/**
* Author form.
*
* @package any
* @subpackage form
* @author Your name here
* @version SVN: $Id: sfPropelFormTemplate.php 10377 2008-07-21 07:10:32Z dwhittle $
*/
class myAuthorForm extends BaseAuthorForm
{
public function configure()
{
$this->setWidget('description', new myWidgetFormRichTextarea(array('editor'=>'fck')));
$values = $this->getValues();
$this->setWidget('pic', new myWidgetFormInputFileEditable(
array(
'is_image' => true,
'delete_label' => 'удалить'
)));
//$this->setWidget('pic', new sfWidgetFormInputFile());
$this->setWidget('avatar', new sfWidgetFormInputFile());
/* VALIDATORS */
$this->setValidators(array(
'f_name' => new sfValidatorString(
array(),
array('required' => 'Обязательное поле')
),
'l_name' => new sfValidatorString(
array(),
array('required' => 'Обязательное поле')
),
'email' => new sfValidatorEmail(
$options = array(),
$messages = array(
'invalid' => 'Введите правильный Email',
'required' => false)
),
'pic' => new sfValidatorFile(array(
'required' => false,
'max_size' => '102400', // bytes (1MB)
'mime_types' => array('image/png', 'image/jpeg',)
)),
));

//$this->widgetSchema->setNameFormat('%s');

$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);
}//configure

public function updateObject($values = null)
{
$object = parent::updateObject();
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.
sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$object->setPic(str_replace($path.'/', '', $object->getPic()));
return $object;
}
/* do save*/
protected function doSave($con = null){
$values = $this->getValues();

$path = sfConfig::get('sf_root_dir').'/'.
sfConfig::get('sf_web_dir_name').'/'.sfConfig::get('sf_upload_dir');

if (isset($values['pic_delete'])){
$currentFile = $path.'/'.$this->getObject()->getPic();
if (is_file($currentFile)){
unlink($currentFile);
}
$this->getObject()->setPic('');
}

$file = $values['pic'];

if(!empty($file)){
if (file_exists(sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getPic())){
@unlink($this->getObject()->getPic());
}
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$file->save($path.'/'.$filename);
}

return parent::doSave($con);
}
}//class

/lib/myWidgetFormInputFileEditable.class.php

/*
* This file is part of the symfony package.
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* sfWidgetFormInputFileEditable represents an upload HTML input tag with the possibility
* to remove a previously uploaded file.
*
* @package symfony
* @subpackage widget
* @author Fabien Potencier
* @version SVN: $Id: sfWidgetFormInputFileEditable.class.php 11544 2008-09-14 17:40:07Z fabien $
*/
class myWidgetFormInputFileEditable extends sfWidgetFormInputFile
{
/**
* Constructor.
*
* Available options:
*
* * file_src: The current image web source path (required)
* * edit_mode: A Boolean: true to enabled edit mode, false otherwise
* * is_image: Whether the file is a displayable image
* * with_delete: Whether to add a delete checkbox or not
* * delete_label: The delete label used by the template
* * template: The HTML template to use to render this widget
* The available placeholders are:
* * input (the image upload widget)
* * delete (the delete checkbox)
* * delete_label (the delete label text)
* * file (the file tag)
*
* In edit mode, this widget renders an additional widget named after the
* file upload widget with a "_delete" suffix. So, when creating a form,
* don't forget to add a validator for this additional field.
*
* @param array $options An array of options
* @param array $attributes An array of default HTML attributes
*
* @see sfWidgetFormInputFile
*/
protected function configure($options = array(), $attributes = array())
{
parent::configure($options, $attributes);

$this->setOption('type', 'file');
$this->setOption('needs_multipart', true);

//$this->addRequiredOption('file_src');
$this->addOption('file_src', '');
$this->addOption('is_image', false);
$this->addOption('edit_mode', true);
$this->addOption('with_delete', true);
$this->addOption('delete_label', 'remove the current file');
$this->addOption('template', '%file% %input% %delete% %delete_label%');
}

/**
* @param string $name The element name
* @param string $value The value displayed in this widget
* @param array $attributes An array of HTML attributes to be merged with the default HTML attributes
* @param array $errors An array of errors for the field
*
* @return string An HTML tag string
*
* @see sfWidgetForm
*/
public function render($name, $value = null, $attributes = array(), $errors = array())
{

$input = parent::render($name, $value, $attributes, $errors);

if (!$this->getOption('edit_mode'))
{
return $input;
}

if ($this->getOption('with_delete')){
$deleteName = ']' == substr($name, -1) ? substr($name, 0, -1).'_delete]' : $name.'_delete';
$delete = $this->renderTag('input', array_merge(array('type' => 'checkbox', 'name' => $deleteName), $attributes));
$deleteLabel = $this->renderContentTag('span', $this->getOption('delete_label'), array_merge(array('for' => $this->generateId($deleteName))));
} else {
$delete = '';
$deleteLabel = '';
}
if ($value){
$picLink = " || <a class='thickbox' target='_blank' href='/uploads/".$value."'>show me</a>";
} else {
$picLink = "";
$delete = '';
$deleteLabel = '';
}

return strtr($this->getOption('template'), array(
'%input%' => $input,
'%delete%' => $delete,
'%delete_label%' => $deleteLabel,
'%file%' => $this->getFileAsTag($attributes, $value))
).$picLink;
}

protected function getFileAsTag($attributes, $value=null)
{
if ($this->getOption('is_image'))
{
return false !== $value ? $this->renderTag('img', array_merge(array('src' => $value)), $attributes) : '';
}
else
{
return $this->getOption('file_src');
}
}
}

/backend/config/view.yml
default:
http_metas:
content-type: text/html
metas:
title: Admin
stylesheets: [main, thickbox]
javascripts: [jq/jquery.pack.js, jq/thickbox.pack.js]

вторник, 11 ноября 2008 г.

Symfony 1.2: вышло обновление - beta2

Очень оперативно работают парни, 10 дней прошло после выхода beta1, и вот вам обновление.
Список "фиксов" тут:
http://trac.symfony-project.org/query?status=closed&milestone=1.2.0+BETA2

Теперь о неприятном ;)
После обновления я сразу получил ошибку
500 | Internal Server Error | sfConfigurationException
The route "articles_collection" does not exist.


Видимо опять что то изменилось в системе роутинга, беглый просмотр офиц.сайта не дал ответа что далать, пришлось "методом научного тыка" сделать костыль.
articles_collection:
url: /article/:action/*

articles:
class: sfPropelRouteCollection
options:
model: Article
module: article
with_show: false
collection_actions: { filter: post, batch: post }

Не знаю, прав ли я???

среда, 29 октября 2008 г.

Symfony admin: изменяем стиль постраничной навигации

Лёгкий тюнинг CSS
.float-right span{
border: 1px solid #ff9900;
padding: 4px;
background: #eeeeff;
}
в /backend/templates/layout.php дает следующий результат

среда, 15 октября 2008 г.

Symfony admin generator: is_published + AJAX

Часто приходится открывать запись на редактирование только ради того что бы поставить галочку "опубликовано"
Сделаем это нажатием на иконку статуса в списке.
Для работы с AJAX используем jQuery.
/backend/config/view.yml

default:
...
javascripts: [jq/jquery.pack.js]


/backend/templates/layout.php

<script>
jQuery(document).ready(function($){
$("body").after("<div id='result_div'></div>");
$("#result_div").html("<img style='padding: 20px' src='/images/loading.gif' align='absmiddle' /> обработка...");
$("#result_div").hide();
$("#result_div").css({
"background-color": "#ffffcc",
"opacity": "0.7",
"width": "40%",
"height": "auto",
"font-family": "Arial Tahoma",
"text-align": "center",
"vertical-align": "middle",
"border": "1px solid silver",
"position": "absolute",
"top": "40%",
"left": "30%",
"z-index": "9999"
});
});
</script>


Изменяем _is_published.php

<center>
<?php echo
link_to(
image_tag(sfConfig::get('sf_admin_web_dir')."/images/".
($myObject->getIsPublished() ? 'ok.png' : 'cancel.png'),
array('id' => 'switch_status_'.$myObject->getId())
),
"my_module/switchStatus?id=".$myObject->getId(),
array(
'class' => 'switch_status',
'rel' => 'switch_status_'.$myObject->getId(),
'pic_dir' => sfConfig::get('sf_admin_web_dir')."/images/",
)
);//link to
?>
</center>


Добавлем файл my_module/templates/_list_footer.php

<script>
$(document).ready(function() {
$(".switch_status").each( function (){
$(this).click( function (){
var popupX = Math.round( ($(window).width() - $("#result_div").width()) / 2) ;
var popupY = $(document).scrollTop() + Math.round($(window).height()/2) -
Math.round($("#result_div").height()/2);
$("#result_div").css({top: popupY+"px", left: popupX+"px"});
var pic_dir = $(this).attr("pic_dir")
var pic_id = $(this).attr("rel")
$("#result_div").slideDown("slow");

$.ajax({
type: "POST",
url: $(this).attr('href'),
data: "",
success: function(msg){
var pic_name = (msg == 1 ? "ok" : "cancel") + ".png"
$("#"+pic_id).attr("src", pic_dir + "/" + pic_name)
$("#result_div").slideUp("slow");
}
});//ajax
return false;
})//click
})//each
});
</script>


Добавляем новый метод executeSwitchStatus в модуль my_module

public function executeSwitchStatus($request){
$myObject = xxxPeer::retrieveByPk($this->getRequestParameter('id'));
$currentStatus = $myObject->getIsPublished();
$newStatus = ($currentStatus == 1 ? 0 : 1);

$myObject->setIsPublished($newStatus);
$myObject->save();

$isAjax = $request->isXmlHttpRequest();

if ($isAjax){
return $this->renderText($newStatus);
} else {
//do something if not AJAX
}//if
}//change status


Получаем

Symfony admin generator: маленькие улучшения

Очень часто в таблицах используется флажок is_published (bool)

Генератор админки его подхватывает, и ставит зеленую галочку, если is_published=1

Мы немного изменим такое поведение.
1. Создаем файл /backend/mymodule/templates/_is_published.php
и копируем в него следующее:

<center>
getIsPublished() ?
image_tag(sfConfig::get('sf_admin_web_dir').'/images/ok.png') :
image_tag(sfConfig::get('sf_admin_web_dir').'/images/cancel.png')
?>
</center>



В generator.yml

list:
fields:
...
is_published: { name: Статус }
...
display: [ ..., _is_published ]


Получаем