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

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

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

Symfony custom widget to display nested tree in listbox

Для формирования древовидной структуры достаточно ввести название для ветки и указать родительскую ветку.


Требования:
- Symfony 1.1.x
- Propel 1.3

/lib/myWidgetFormPropelSelectTree.class.php

<?php

/*
* widget displays nested tree in listbox
* based on symfony 1.1 and Propel 1.3
* 28-okt-2008 by Vit <228vit@gmail.com>
*/
class myWidgetFormPropelSelectTree extends sfWidgetFormSelect
{
/**
* @see sfWidget
*/
public function __construct($options = array(), $attributes = array())
{
$options['choices'] = new sfCallable(array($this, 'getChoices'));

parent::__construct($options, $attributes);
}

/**
* Constructor.
*
* Available options:
*
* * model: The model class (required)
* * add_empty: Whether to add a first empty value or not (false by default)
* If the option is not a Boolean, the value will be used as the text value
* * method: The method to use to display object values (__toString by default)
* * order_by: An array composed of two fields:
* * The column to order by the results (must be in the PhpName format)
* * asc or desc
* * criteria: A criteria to use when retrieving objects
* * connection: The Propel connection to use (null by default)
* * multiple: true if the select tag must allow multiple selections
*
* @see sfWidgetFormSelect
*/
protected function configure($options = array(), $attributes = array())
{
$this->addRequiredOption('model');
$this->addOption('add_empty', false);
$this->addOption('method', '__toString');
$this->addOption('order_by', null);//no needed, tree must be sorted
$this->addOption('criteria', null);
$this->addOption('connection', null);
$this->addOption('multiple', false);

parent::configure($options, $attributes);
}

/**
* Returns the choices associated to the model.
*
* @return array An array of choices
*/
public function getChoices()
{
$choices = array();
if (false !== $this->getOption('add_empty'))
{
$choices[''] = true === $this->getOption('add_empty') ? '' : $this->getOption('add_empty');
}

$class = $this->getOption('model').'Peer';
$criteria = is_null($this->getOption('criteria')) ? new Criteria() : clone $this->getOption('criteria');

$method = $this->getOption('method');

if (!method_exists($this->getOption('model'), $method))
{
throw new RuntimeException(sprintf('Class "%s" must implement a "%s" method to be rendered in a "%s" widget', $this->getOption('model'), $method, __CLASS__));
}
//get all SCOPEs from a tree
$cr = new Criteria;
$method2 = 'addAscendingOrderByColumn';
$cr->addAscendingOrderByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$cr->addGroupByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$scopes = call_user_func(array($class, 'doSelect'), $cr, $this->getOption('connection'));
$scopesCnt = count($scopes)-1;
//loop over scopes
foreach ($scopes as $currScope => $scope){
//retrieve tree for current scope
//TreePeer::retrieveTree($scope->getScope());
$tree = call_user_func(array($class, 'retrieveTree'), $scope->getScope(), $this->getOption('connection'));
//start iterator
$it = new myTreeListboxOptions($tree);
//loop over childs
foreach ($it as $m){
//store attributes in arrays, to build row for listbox
$siblings[$m->getLevel()] = intval($m->hasNextSibling());
$childs[$m->getLevel()] = intval($m->hasChildren());
$currLevel = $m->getLevel();
$connectors = "";//extra string which contains connectors:


В форме описываем как:

$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
...
'parent' => new myWidgetFormPropelSelectTree(
array(
'model' => 'Menu',
'add_empty' => 'root')),
));

$this->widgetSchema->setNameFormat('%s');
$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);


В пример для обработки в действии:

   if ($this->form->isValid()){
$tree = new Menu();
$tree->setName($request->getParameter('name'));
$parent = $request->getParameter('parent');

//get max scope
$nextScope = 0;
$c = new Criteria;
$c->clearSelectColumns()->addSelectColumn('MAX(' . MenuPeer::SCOPE . ')');
$stmt = MenuPeer::doSelectStmt($c);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$nextScope = $row[0] + 1;
}
$nextScope = (is_null($nextScope) ? 1 : $nextScope);

if (empty($parent)){
$tree->makeRoot();
$tree->setScopeIdValue($nextScope);
} else {
$root = MenuPeer::retrieveByPK($parent);
$tree->insertAsLastChildOf($root);
}
$tree->save();
}//if valid

понедельник, 20 октября 2008 г.

jQuery: custom popup on center screen

При обработке событий джаваскриптом, часто надо показывать слой с результатом работы и ходом исполнения по центру экрана. Проблемы вычисления центра возникают если экран имеет вертикальную прокрутку, вот решение:

$(document).ready(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"});
$("#result_div").slideDown("slow");
...

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

Алфавит циклом, в кодировке UTF-8

Выбор по алфавиту:

<?php for($i = 144; $i <= 175; $i++): ?>
<a href="getModuleName()."/list?letter=".chr($i)) ?>">
<?php echo chr(208).chr($i)?></a>
<?php endfor; ?>


Получаем:
А Б В Г Д Е Ж З И Й ...

Буква Ё отсутствует, восстановим справедливость:

<?php for($i = 144; $i <= 175; $i++): ?>
<a href="<?php echo url_for($this->getModuleName()."/list?letter=".chr(208).chr($i)) ?>">
<?php echo chr(208).chr($i)?></a>
<?php if ($i == 149): ?>
<a href="<?php echo url_for($this->getModuleName()."/list?letter=".chr(208).chr(129)) ?>">
<?php echo chr(208).chr(129)?></a>
<?php endif; ?>
<?php endfor; ?>

Propel 1.3: маленькая проблема при работе с nested sets

Если вы вдруг в процессе разработки проекта решили, что самое время перейти на Propel 1.3, для использования его встроенной поддержки древовидных структур(nested sets), не наступите на мои "грабли", а именно:
после установки Propel 1.3, внесения изменений в таблицу и регенерации модели, убедитесь, что ваша старая модель(в моем случае /lib/model/Categorie.php) стала наследовать новую, пример:

//было
class Categorie extends BaseCategorie
//стало
class Categorie extends BaseCategorieNestedSet

Дело в том, что генератор Propel увидев, что я вносил изменения в /lib/model/Categorie.php не стал его трогать, и менять extends BaseCategorie, в результате я получил ошибки, при попытке использовать методы типа:
$root->makeRoot();
$menu->insertAsLastChildOf($root);
Доверяй генератору, но проверяй ;)

среда, 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 ]


Получаем

суббота, 11 октября 2008 г.

Symfony: Propel 1.3 installation

Не сразу заметен пробел после /1.3/

$ cd /path/to/project/root/
$ svn co http://svn.symfony-project.com/plugins/sfPropelPlugin/branches/1.3/[::space::]plugins/sfPropelPlugin

Пользователям "черепашки" обратить внимание.

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

Symfony: admin icons set

Незаменимая коллекция иконок для дополнительных кнопок, для админки и других интерфейсов.

famfam

Symfony: Propel unique validator


$this->validatorSchema->setPostValidator(new
sfValidatorPropelUnique(array(
'model' => 'Users',
'column' => array('login')),
array(
'invalid' => 'Такой логин уже есть.'
)
));

понедельник, 6 октября 2008 г.

Symfony custom log

Только для frontend.php получаем ошибку

Fatal error: Cannot redeclare class sfLogger in C:\www\Apache2\htdocs\sfprojects\XXX\cache\frontend\prod\config\config_core_compile.yml.php on line 1211

Лекарство:


require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', true);
sfContext::createInstance($configuration)->dispatch();