воскресенье, 20 апреля 2008 г.

Как построить поисковый движок?

Как построить поисковый движок?


Добавляем новую таблицу SearchIndex в schema.yml и перестраиваем модель:


ask_search_index:

ask_search_index:
_attributes:
phpName: SearchIndex
question_id:
type: integer
foreignTable: ask_question
foreignReference: id
onDelete: cascade
word:
type: varchar
size: 255
index: true
weight:
type: integer


Атрибут onDelete котролирует все удаления в таблице поиска SearchIndex при удалении соответствующих записей в таблице вопрос.


Разбиение фразы на слова


Некоторые слова, такие как "a," "of," "the," "I,", "it", "you," и т.д. следует исключить из поисковой таблицы.


Добавим следующий метод в класс /lib/myTools.class.php:


public static function removeStopWordsFromArray($words)

{

$stop_words = array(

'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours',

'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers',

'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves',

'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are',

'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does',

'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until',

'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into',

'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down',

'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here',

'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more',

'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so',

'than', 'too', 'very',

);



return array_diff($words, $stop_words);

}


в этот же класс добавляем метод для разбиения фраз на слова



public static function stemPhrase($phrase)

{

// split into words

$words = str_word_count(strtolower($phrase), 1);


// ignore stop words

$words = myTools::removeStopWordsFromArray($words);


// stem words

$stemmed_words = array();

foreach ($words as $word)

{

// ignore 1 and 2 letter words

if (strlen($word) <= 2)

{

continue;

}


$stemmed_words[] = PorterStemmer::stem($word, true);

}


return $stemmed_words;

}

В этом методе используется Porter Stemming Algorithm, реализацию которого можно взять с tartarus.org(http://www.tartarus.org/~martin/PorterStemmer/php.txt) и разместить под именем PorterStemmer.class.php в том же каталоге, где располагается myTools.class.php.


Получаем вес слова


Используем такой алгоритм:


Слово имеет больший вес, если оно встречается в заголовке.

Слово получает больший вес, если оно встречается дважды в содержимом до появления других слов, встретившихся уже один раз.


/apps/frontend/config/app.yml):


all:
...

search:
body_weight: 1
title_weight: 2
tag_weight: 3

Реализация алгоритма выполняется в файле /lib/model/Question.php:


public function save($con = null)

{

$con = sfContext::getInstance()->getDatabaseConnection('propel');

try

{

$con->begin();



$ret = parent::save($con);

$this->updateSearchIndex();



$con->commit();



return $ret;

}

catch (Exception $e)

{

$con->rollback();

throw $e;

}

}



public function updateSearchIndex()

{

// delete existing SearchIndex entries about the current question

$c = new Criteria();

$c->add(SearchIndexPeer::QUESTION_ID, $this->getId());

SearchIndexPeer::doDelete($c);



// create a new entry for each of the words of the question

foreach ($this->getWords() as $word => $weight)

{

$index = new SearchIndex();

$index->setQuestionId($this->getId());

$index->setWord($word);

$index->setWeight($weight);

$index->save();

}

}



public function getWords()

{

// body

$raw_text = str_repeat(' '.strip_tags($this->getHtmlBody()), sfConfig::get('app_search_body_weight'));



// title

$raw_text .= str_repeat(' '.$this->getTitle(), sfConfig::get('app_search_title_weight'));



// title and body stemming

$stemmed_words = myTools::stemPhrase($raw_text);



// unique words with weight

$words = array_count_values($stemmed_words);



// add tags

$max = 0;

foreach ($this->getPopularTags(20) as $tag => $count)

{

if (!$max)

{

$max = $count;

}



$stemmed_tag = PorterStemmer::stem($tag);



if (!isset($words[$stemmed_tag]))

{

$words[$stemmed_tag] = 0;

}

$words[$stemmed_tag] += ceil(($count / $max) * sfConfig::get('app_search_tag_weight'));

}



return $words;

}


Мы также должны обновлять индекс вопросов всякий раз , когда происхлдит добавление. Поэтому перепишем метод save() модели Tag:


public function save($con = null)

{

$con = sfContext::getInstance()->getDatabaseConnection('propel');

try

{

$con->begin();



$ret = parent::save($con);

$this->getQuestion()->updateSearchIndex();



$con->commit();



return $ret;

}

catch (Exception $e)

{

$con->rollback();

throw $e;

}

}


Индекс построен, протестируем его


$ php batch/load_data.php




public static function search($phrase, $exact = false, $offset = 0, $max = 10)

{

$words = array_values(myTools::stemPhrase($phrase));

$nb_words = count($words);



if (!$words)

{

return array();

}



$con = sfContext::getInstance()->getDatabaseConnection('propel');



// define the base query

$query = '

SELECT DISTINCT '.SearchIndexPeer::QUESTION_ID.', COUNT(*) AS nb, SUM('.SearchIndexPeer::WEIGHT.') AS total_weight

FROM '.SearchIndexPeer::TABLE_NAME;



if (sfConfig::get('app_permanent_tag'))

{

$query .= '

WHERE ';

}

else

{

$query .= '

LEFT JOIN '.QuestionTagPeer::TABLE_NAME.' ON '.QuestionTagPeer::QUESTION_ID.' = '.SearchIndexPeer::QUESTION_ID.'

WHERE '.QuestionTagPeer::NORMALIZED_TAG.' = ? AND ';

}



$query .= '

('.implode(' OR ', array_fill(0, $nb_words, SearchIndexPeer::WORD.' = ?')).')

GROUP BY '.SearchIndexPeer::QUESTION_ID;



// AND query?

if ($exact)

{

$query .= '

HAVING nb = '.$nb_words;

}



$query .= '

ORDER BY nb DESC, total_weight DESC';



// prepare the statement

$stmt = $con->prepareStatement($query);

$stmt->setOffset($offset);

$stmt->setLimit($max);

$placeholder_offset = 1;

if (sfConfig::get('app_permanent_tag'))

{

$stmt->setString(1, sfConfig::get('app_permanent_tag'));

$placeholder_offset = 2;

}

for ($i = 0; $i < $nb_words; $i++)

{

$stmt->setString($i + $placeholder_offset, $words[$i]);

}

$rs = $stmt->executeQuery(ResultSet::FETCHMODE_NUM);



// Manage the results

$questions = array();

while ($rs->next())

{

$questions[] = self::retrieveByPK($rs->getInt(1));

}



return $questions;

}


Поисковая форма


// add to defaultSuccess.php and questionSuccess.php in askeet/apps/frontend/modules/sidebar/templates/

<h2>find it</h2>

<?php include_partial('question/search') ?>



// create the following askeet/apps/frontend/modules/question/templates/_search.php fragment

<?php echo form_tag('@search_question') ?>

<?php echo input_tag('search', htmlspecialchars($sf_params->get('search')), array('style' => 'width: 150px')) ?>&nbsp;

<?php echo submit_tag('search it', 'class=small') ?>

<?php echo checkbox_tag('search_all', 1, $sf_params->get('search_all')) ?>&nbsp;<label for="search_all" class="small">search with all words</label>

</form>




Определим правило @search_question в routing.yml:


search_question:
url: /search/*
param: { module: question, action: search }


QuestionPeer::search() метод:


public function executeSearch ()

{

if ($this->getRequestParameter('search'))

{

$this->questions = QuestionPeer::search($this->getRequestParameter('search'), $this->getRequestParameter('search_all', false), ($this->getRequestParameter('page', 1) - 1) * sfConfig::get('app_search_results_max'), sfConfig::get('app_search_results_max'));

}

else

{

$this->redirect('@homepage');

}

}



app.yml file:


all:
search:
results_max: 10

Результат поиска


/apps/frontend/modules/question/templates/searchSuccess.php. :


<?php use_helper('Global') ?>



<h1>questions matching "<?php echo htmlspecialchars($sf_params->get('search')) ?>"</h1>



<?php foreach($questions as $question): ?>

<?php include_partial('question/question_block', array('question' => $question)) ?>

<?php endforeach ?>



<?php if ($sf_params->get('page') > 1 && !count($questions)): ?>

<div>There is no more result for your search.</div>

<?php elseif (!count($questions)): ?>

<div>Sorry, there is no question matching your search terms.</div>

<?php endif ?>



<?php if (count($questions) == sfConfig::get('app_search_results_max')): ?>

<div class="right">

<?php echo link_to('more results &raquo;', '@search_question?search='.$sf_params->get('search').'&page='.($sf_params->get('page', 1) + 1)) ?>

</div>

<?php endif ?>


суббота, 19 апреля 2008 г.

Строим свои хелперы

Строим свои хелперы


Создаем файл GlobalHelper.php в каталоге askeet/apps/frontend/lib/helper:


<?php



function pager_navigation($pager, $uri)

{

$navigation = '';



if ($pager->haveToPaginate())

{

$uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';



// First and previous page

if ($pager->getPage() != 1)

{

$navigation .= link_to(image_tag('first.gif', 'align=absmiddle'), $uri.'1');

$navigation .= link_to(image_tag('previous.gif', 'align=absmiddle'), $uri.$pager->getPreviousPage()).'&nbsp;';

}



// Pages one by one

$links = array();

foreach ($pager->getLinks() as $page)

{

$links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);

}

$navigation .= join('&nbsp;&nbsp;', $links);



// Next and last page

if ($pager->getPage() != $pager->getCurrentMaxLink())

{

$navigation .= '&nbsp;'.link_to(image_tag('next.gif', 'align=absmiddle'), $uri.$pager->getNextPage());

$navigation .= link_to(image_tag('last.gif', 'align=absmiddle'), $uri.$pager->getLastPage());

}



}



return $navigation;

}


Для использования этого хелпера в фрагменте шаблона question/templates/_list.php вызываем его так:


<?php use_helper('Text', 'Global') ?>



<?php foreach($question_pager->getResults() as $question): ?>

<div class="question">

<div class="interested_block">

<?php include_partial('interested_user', array('question' => $question)) ?>

</div>



<h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>



<div class="question_body">

<?php echo truncate_text($question->getBody(), 200) ?>

</div>

</div>

<?php endforeach; ?>



<div id="question_pager">

<?php echo pager_navigation($question_pager, 'question/list') ?>

</div>


Список часто задаваемых вопросов


В модуле question создаем новое действие:


public function executeRecent()

{

$this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));

}


Добавим в класс /lib/model/QuestionPeer.php:


public static function getRecentPager($page)

{

$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));

$c = new Criteria();

$c->addDescendingOrderByColumn(self::CREATED_AT);

$pager->setCriteria($c);

$pager->setPage($page);

$pager->setPeerMethod('doSelectJoinUser');

$pager->init();



return $pager;

}


Шаблон /apps/frontend/module/question/templates/recentSuccess.php:


<h1>recent questions</h1>



<?php include_partial('list', array('question_pager' => $question_pager)) ?>


Добавим recent_questions правило в frontend/config/routing.yml :


recent_questions:
url: /question/recent/:page
param: { module: question, action: recent, page: 1 }

Меняем последнюю строку в recentSuccess.php на:


<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>


а также меняем последнюю строку в _list.php на fragment to:


<div id="question_pager">

<?php echo pager_navigation($question_pager, $rule) ?>

</div>


вызываем _list в modules/question/templates/listSuccess.php.


<h1>popular questions</h1>



<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>


http://ваш хост/question/recent


Список частых ответов


Созаем модуль:


$ symfony init-module frontend answer


Создаем действие:


public function executeRecent()

{

$this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1));

}


Добавим в классAnswerPeer:


public static function getRecentPager($page)

{

$pager = new sfPropelPager('Answer', sfConfig::get('app_pager_homepage_max'));

$c = new Criteria();

$c->addDescendingOrderByColumn(self::CREATED_AT);

$pager->setCriteria($c);

$pager->setPage($page);

$pager->setPeerMethod('doSelectJoinUser');

$pager->init();



return $pager;

}


Шаблон recentSuccess.php:


<?php use_helper('Date', 'Global') ?>



<h1>recent answers</h1>



<div id="answers">

<?php foreach ($answer_pager->getResults() as $answer): ?>

<div class="answer">

<h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?stripped_title='.$answer->getQuestion()->getStrippedTitle()) ?></h2>

<?php echo count($answer->getRelevancys()) ?> points

posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?>

on <?php echo format_date($answer->getCreatedAt(), 'p') ?>

<div>

<?php echo $answer->getBody() ?>

</div>

</div>

<?php endforeach ?>

</div>



<div id="question_pager">

<?php echo pager_navigation($answer_pager, 'answer/recent') ?>

</div>



Профиль пользователя


Для модуля user/show создаем действие


public function executeShow()

{

$this->subscriber = UserPeer::retrieveByPk($this->getRequestParameter('id', $this->getUser()->getSubscriberId()));

$this->forward404Unless($this->subscriber);



$this->interests = $this->subscriber->getInterestsJoinQuestion();

$this->answers = $this->subscriber->getAnswersJoinQuestion();

$this->questions = $this->subscriber->getQuestions();

}


Шаблон /apps/frontend/modules/user/templates/showSuccess.php:


<h1><?php echo $subscriber ?>�s profile</h1>



<h2>Interests</h2>



<ul>

<?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>

<li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>

<?php endforeach; ?>

</ul>



<h2>Contributions</h2>



<ul>

<?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>

<li>

<?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?><br />

<?php echo $answer->getBody() ?>

</li>

<?php endforeach; ?>

</ul>



<h2>Questions</h2>



<ul>

<?php foreach ($questions as $question): ?>

<li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>

<?php endforeach; ?>

</ul>


Добавим следующие строки в question/templates/showSuccess.php и question/templates/_list.php вначале question_body div:


<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>


Определим Date helper в _list.php.


Добавим навигационный бар


Меняем layout .


/apps/frontend/templates/layout.php


<div id="content_bar">

<?php include_component_slot('sidebar') ?>

<div class="verticalalign"></div>

</div>


view.yml:


default:
components:
sidebar: [sidebar, default]

$ symfony init-module frontend sidebar


apps/frontend/modules/sidebar/actions/ переимменуем actions.class.php в components.class.php и изменим содержимоена:


<?php



class sidebarComponents extends sfComponents

{

public function executeDefault()

{

}

}


Создадим /apps/frontend/modules/sidebar/templates/_default.php :


<?php echo link_to('ask a new question', 'question/add') ?>



<ul>

<li><?php echo link_to('popular questions', 'question/list') ?></li>

<li><?php echo link_to('latest questions', 'question/recent') ?></li>

<li><?php echo link_to('latest answers', 'answer/recent') ?></li>

</ul>


$ symfony clear-cache


view.yml :


default:
http_metas:
content-type: text/html; charset=utf-8

 metas:
title: symfony project
robots: index, follow
description: symfony project
keywords: symfony, project
language: en

 stylesheets:    [main, layout]

 javascripts:    []

 has_layout:     on
layout: layout

 components:
sidebar: [sidebar, default]

Меняем:


 metas:
title: askeet! ask questions, find answers
robots: index, follow
description: askeet!, a symfony project built in 24 hours
keywords: symfony, project, askeet, php5, question, answer
language: en

 

Исходные коды к блогу Вопрос - ответ

data\fixtures test_data.yml


User:
anonymous:
nickname: anonymous
first_name: Anonymous
last_name: Coward

fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier

francois:
nickname: francoisz
first_name: François
last_name: Zaninotto

Question:
q1:
title: What shall I do tonight with my girlfriend?
user_id: fabien
body: |
We shall meet in front of the Dunkin'Donuts before dinner,
and I haven't the slightest idea of what I can do with her.
She's not interested in programming, space opera movies nor insects.
She's kinda cute, so I really need to find something
that will keep her to my side for another evening.

q2:
title: What can I offer to my step mother?
user_id: anonymous
body: |
My stepmother has everything a stepmother is usually offered
(watch, vacuum cleaner, earrings, del.icio.us account).
Her birthday comes next week, I am broke, and I know that
if I don't offer her something sweet, my girlfriend
won't look at me in the eyes for another month.

q3:
title: How can I generate traffic to my blog?
user_id: francois
body: |
I have a very swell blog that talks
about my class and mates and pets and favorite movies.

Interest:
i1: { user_id: fabien, question_id: q1 }
i2: { user_id: francois, question_id: q1 }
i3: { user_id: francois, question_id: q2 }
i4: { user_id: fabien, question_id: q2 }



batch/load_data.php

<?php


define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..'));

define('SF_APP', 'frontend');

define('SF_ENVIRONMENT', 'dev');

define('SF_DEBUG', true);


require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');


// initialize database manager

$databaseManager = new sfDatabaseManager();

$databaseManager->initialize();


$data = new sfPropelData();

$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');


?>




schema.xml

  <?xml version="1.0" encoding="UTF-8" ?>


- <database name="propel" defaultIdMethod="native" noxsd="true">



- <table name="ask_question" phpName="Question">



  <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />



  <column name="user_id" type="integer" />



- <foreign-key foreignTable="ask_user">



  <reference local="user_id" foreign="id" />


  </foreign-key>




  <column name="title" type="longvarchar" />



  <column name="body" type="longvarchar" />



  <column name="created_at" type="timestamp" />



  <column name="updated_at" type="timestamp" />


  </table>




- <table name="ask_answer" phpName="Answer">



  <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />



  <column name="question_id" type="integer" />



- <foreign-key foreignTable="ask_question">



  <reference local="question_id" foreign="id" />


  </foreign-key>




  <column name="user_id" type="integer" />



- <foreign-key foreignTable="ask_user">



  <reference local="user_id" foreign="id" />


  </foreign-key>




  <column name="body" type="longvarchar" />



  <column name="created_at" type="timestamp" />


  </table>




- <table name="ask_user" phpName="User">



  <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />



  <column name="nickname" type="varchar" size="50" />



  <column name="first_name" type="varchar" size="100" />



  <column name="last_name" type="varchar" size="100" />



  <column name="created_at" type="timestamp" />


  </table>




- <table name="ask_interest" phpName="Interest">



  <column name="question_id" type="integer" primaryKey="true" />



- <foreign-key foreignTable="ask_question">



  <reference local="question_id" foreign="id" />


  </foreign-key>




  <column name="user_id" type="integer" primaryKey="true" />



- <foreign-key foreignTable="ask_user">



  <reference local="user_id" foreign="id" />


  </foreign-key>




  <column name="created_at" type="timestamp" />


  </table>




- <table name="ask_relevancy" phpName="Relevancy">



  <column name="answer_id" type="integer" primaryKey="true" />



- <foreign-key foreignTable="ask_answer">



  <reference local="answer_id" foreign="id" />


  </foreign-key>




  <column name="user_id" type="integer" primaryKey="true" />



- <foreign-key foreignTable="ask_user">



  <reference local="user_id" foreign="id" />


  </foreign-key>




  <column name="score" type="integer" />



  <column name="created_at" type="timestamp" />


  </table>



  </database>





actions.class.php


<?php

// auto-generated by sfPropelCrud

// date: 2006/05/18 11:06:58

?>

<?php


/**

* question actions.

*

* @package ##PROJECT_NAME##

* @subpackage question

* @author Your name here

* @version SVN: $Id: actions.class.php 11 2006-05-18 10:00:25Z fabien $

*/

class questionActions extends sfActions

{

public function executeList ()

{

$this->questions = QuestionPeer::doSelect(new Criteria());

}


public function executeShow ()

{

$this->question = Que

listSuccess.php


<?php use_helper('Text') ?>


<h1>popular questions</h1>


<?php foreach($questions as $question): ?>

<div class="question">

<div class="interested_block">

<div class="interested_mark" id="interested_in_<?php echo $question->getId() ?>">

<?php echo count($question->getInterests()) ?>

</div>

</div>


<h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>


<div class="question_body">

<?php echo truncate_text($question->getBody(), 200) ?>

</div>

</div>

<?php endforeach; ?>


stionPeer::retrieveByPk($this->getRequestParameter('id'));

$this->forward404Unless($this->question);

}

}


?>




showSuccess.php


<?php

// auto-generated by sfPropelCrud

// date: 2006/05/18 11:06:58

?>

<table>

<tbody>

<tr>

<th>Id: </th>

<td><?php echo $question->getId() ?></td>

</tr>

<tr>

<th>User: </th>

<td><?php echo $question->getUserId() ?></td>

</tr>

<tr>

<th>Title: </th>

<td><?php echo $question->getTitle() ?></td>

</tr>

<tr>

<th>Body: </th>

<td><?php echo $question->getBody() ?></td>

</tr>

<tr>

<th>Created at: </th>

<td><?php echo $question->getCreatedAt() ?></td>

</tr>

<tr>

<th>Updated at: </th>

<td><?php echo $question->getUpdatedAt() ?></td>

</tr>

</tbody>

</table>

<hr />

<?php echo link_to('edit', 'question/edit?id='.$question->getId()) ?>

&nbsp;<?php echo link_to('list', 'question/list') ?>

пятница, 18 апреля 2008 г.

Проверка формы

Проверка формы Login

Валидационный файл

Создаем login.yml в /frontend/modules/user/validate:



methods:
post: [nickname, password]

names:
nickname:
required: true
required_msg: your nickname is required
validators: nicknameValidator

password:
required: true
required_msg: your password is required

nicknameValidator:
class: sfStringValidator
param:
min: 5
min_error: nickname must be 5 or more characters

public function handleError()

{

return sfView::ERROR;

}


That's a whole new template to write. But we'd rather display the login form again, with the error messages displayed close to the problematic fields. So let's modify the login error behaviour to display, in this case, the loginSuccess.php template:


public function handleErrorLogin()

{

return sfView::SUCCESS;

}


The naming conventions that link the action name, its return value and the template file name are exposed in the view chapter of the symfony book.


Шаблон хелпера error


При повторном вызове loginSuccess.php необходимо выводить сообщение об ошибке. Изменим две строки формы на:


<?php use_helper('Validation') ?>



<div class="form-row">

<?php echo form_error('nickname') ?>

<label for="nickname">nickname:</label>

<?php echo input_tag('nickname', $sf_params->get('nickname')) ?>

</div>



<div class="form-row">

<?php echo form_error('password') ?>

<label for="password">password:</label>

<?php echo input_password_tag('password') ?>

</div>



Стили ошибок


Определим класс .form_error в web/main.css:


.form_error

{

padding-left: 85px;

color: #d8732f;

}


Авторизация пользователя


Редактируем login.yml:


...
...
names:
nickname:
required: true
required_msg: your nickname is required
validators: [nicknameValidator, userValidator]
...
userValidator:
class: myLoginValidator
param:
password: password
login_error: this account does not exist or you entered a wrong password

Хранение пароля


Меняем schema.xml:


<column name="email" type="varchar" size="100" />

<column name="sha1_password" type="varchar" size="40" />

<column name="salt" type="varchar" size="32" />


Перестраиваем модель Добавляем в/lib/model/User.php метод setPassword():


public function setPassword($password)

{

$salt = md5(rand(100000, 999999).$this->getNickname().$this->getEmail());

$this->setSalt($salt);

$this->setSha1Password(sha1($salt.$password));

}


Добавим пароль в тестовые данные


/data/fixtures/test_data.yml:



User:
...
fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier
password: symfony
email: fp@example.com

francois:
nickname: francoisz
first_name: François
last_name: Zaninotto
password: adventcal
email: fz@example.com

$ php batch/load_data.php


Напишем myLoginValidator. Его можно создать в любой директории lib/ (/lib/, /apps/frontend/lib/,/apps/frontend/modules/user/lib/). Например создадим myLoginValidator.class.php в /apps/frontend/lib/:


<?php



class myLoginValidator extends sfValidator

{

public function initialize($context, $parameters = null)

{

// initialize parent

parent::initialize($context);



// set defaults

$this->setParameter('login_error', 'Invalid input');



$this->getParameterHolder()->add($parameters);



return true;

}



public function execute(&$value, &$error)

{

$password_param = $this->getParameter('password');

$password = $this->getContext()->getRequest()->getParameter($password_param);



$login = $value;



// anonymous is not a real user

if ($login == 'anonymous')

{

$error = $this->getParameter('login_error');

return false;

}



$c = new Criteria();

$c->add(UserPeer::NICKNAME, $login);

$user = UserPeer::doSelectOne($c);



// nickname exists?

if ($user)

{

// password is OK?

if (sha1($user->getSalt().$password) == $user->getSha1Password())

{

$this->getContext()->getUser()->setAuthenticated(true);

$this->getContext()->getUser()->addCredential('subscriber');



$this->getContext()->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');

$this->getContext()->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');



return true;

}

}



$error = $this->getParameter('login_error');

return false;

}

}



Изменим:


public function executeLogin()

{

if ($this->getRequest()->getMethod() != sfRequest::POST)

{

// display the form

$this->getRequest()->getParameterHolder()->set('referer', $this->getRequest()->getReferer());



return sfView::SUCCESS;

}

else

{

// handle the form submission

// redirect to last page

return $this->redirect($this->getRequestParameter('referer', '@homepage'));

}

}



Запрет доступа

/apps/frontend/modules/question/config/security.yml :


add:
is_secure: on
credentials: subscriber

all:
is_secure: off

Оптимизация


Добавим метод в /apps/frontend/lib/myUser.php :


public function signIn($user)

{

$this->setAttribute('subscriber_id', $user->getId(), 'subscriber');

$this->setAuthenticated(true);



$this->addCredential('subscriber');

$this->setAttribute('nickname', $user->getNickname(), 'subscriber');

}



public function signOut()

{

$this->getAttributeHolder()->removeNamespace('subscriber');



$this->setAuthenticated(false);

$this->clearCredentials();

}


Изменим 4 строки начиная с $this->getContext()->getUser() в myLoginValidator классе на:


$this->getContext()->getUser()->signIn($user);


Изменим user/logout децствие на:


public function executeLogout()

{

$this->getUser()->signOut();



$this->redirect('@homepage');

}


В классе myUser добавим метод:


public function getSubscriberId()

{

return $this->getAttribute('subscriber_id', '', 'subscriber');

}



public function getSubscriber()

{

return UserPeer::retrieveByPk($this->getSubscriberId());

}



public function getNickname()

{

return $this->getAttribute('nickname', '', 'subscriber');

}


Используем его в layout.php:

сменив

<li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>



на

<li><?php echo link_to($sf_user->getNickname().' profile', 'user/profile') ?></li>



Форма Login

Форма Login


Изменим глобальній шаблон /apps/frontend/templates/layout.php, добавив ссілку на регистрацию:

<li><?php echo link_to('sign in', 'user/login') ?></li>


Создадим модуль пользователь

$ symfony init-module frontend user


Создадим user/login action


в /apps/frontend/modules/user/actions/action.class.php добавим:


public function executeLogin()

{

$this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());

return sfView::SUCCESS;

}



Создадим шаблон loginSuccess.php


В /apps/frontend/modules/user/templates/ создаем файл loginSuccess.php:


<?php echo form_tag('user/login') ?>



<fieldset>



<div class="form-row">

<label for="nickname">nickname:</label>

<?php echo input_tag('nickname', $sf_params->get('nickname')) ?>

</div>



<div class="form-row">

<label for="password">password:</label>

<?php echo input_password_tag('password') ?>

</div>



</fieldset>



<?php echo input_hidden_tag('referer', $sf_request->getAttribute('referer')) ?>

<?php echo submit_tag('sign in') ?>



</form>



По правилам формі действие візівается, когда нажата кнопка submit


Изменим код executeLogin():


public function executeLogin()

{

if ($this->getRequest()->getMethod() != sfRequest::POST)

{

// display the form

$this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());

}

else

{

// handle the form submission

$nickname = $this->getRequestParameter('nickname');



$c = new Criteria();

$c->add(UserPeer::NICKNAME, $nickname);

$user = UserPeer::doSelectOne($c);



// nickname exists?

if ($user)

{

// password is OK?

if (true)

{

$this->getUser()->setAuthenticated(true);

$this->getUser()->addCredential('subscriber');



$this->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');

$this->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');



// redirect to last page

return $this->redirect($this->getRequestParameter('referer', '@homepage'));

}

}

}

}



Привилегии устанавливаются здесь с помощью

$this->getContext()->getUser()->setAuthenticated(true);

$this->getContext()->getUser()->addCredential('subscriber');



Добавим функцию user/logout


public function executeLogout()

{

$this->getUser()->setAuthenticated(false);

$this->getUser()->clearCredentials();



$this->getUser()->getAttributeHolder()->removeNamespace('subscriber');



$this->redirect('@homepage');

}


Изменим layout


/apps/frontend/templates/layout.php:


<?php if ($sf_user->isAuthenticated()): ?>

<li><?php echo link_to('sign out', 'user/logout') ?></li>

<li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>

<?php else: ?>

<li><?php echo link_to('sign in/register', 'user/login') ?></li>

<?php endif ?>



Постраничное листание Question


Изменим действие question/list


public function executeList ()

{

$this->questions = QuestionPeer::doSelect(new Criteria());

}



Воспользуемся обїектом sfPropelPager:


public function executeList ()

{

$pager = new sfPropelPager('Question', 2);

$c = new Criteria();

$c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS);

$pager->setCriteria($c);

$pager->setPage($this->getRequestParameter('page', 1));

$pager->setPeerMethod('doSelectJoinUser');

$pager->init();



$this->question_pager = $pager;

}


Конфигурацию листания зададим в /apps/frontend/config/app.yml:

  
all:
pager:
homepage_max: 2

Реализация этого - заменим new sfPropelPager line above by:

на

$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));



Также модифицируем шаблон listSuccess.php


Заменим строку

<?php foreach($questions as $question): ?>

на

<?php foreach($question_pager->getResults() as $question): ?>



Организуем навигацию


В конец шаблона добавим

<div id="question_pager">

<?php if ($question_pager->haveToPaginate()): ?>

<?php echo link_to('&laquo;', 'question/list?page=1') ?>

<?php echo link_to('&lt;', 'question/list?page='.$question_pager->getPreviousPage()) ?>



<?php foreach ($question_pager->getLinks() as $page): ?>

<?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?>

<?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?>

<?php endforeach; ?>



<?php echo link_to('&gt;', 'question/list?page='.$question_pager->getNextPage()) ?>

<?php echo link_to('&raquo;', 'question/list?page='.$question_pager->getLastPage()) ?>

<?php endif; ?>

</div>


Добавим routing правила


в apps/frontend/config/routing.yml добавим сверху:


popular_questions:

url: /index/:page

param: { module: question, action: list }


И для страницы login:


login:

url: /login

param: { module: user, action: login }


Оптимизация


Заменим question/list :


public function executeList ()

{

$this->question_pager = QuestionPeer::getHomepagePager($this->getRequestParameter('page', 1));

}


И добавим следующий метод в класс QuestionPeer.php модели lib/model:


public static function getHomepagePager($page)

{

$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));

$c = new Criteria();

$c->addDescendingOrderByColumn(self::INTERESTED_USERS);

$pager->setCriteria($c);

$pager->setPage($page);

$pager->setPeerMethod('doSelectJoinUser');

$pager->init();



return $pager;

}


Тоже самое касается действия question/show: Заменим действие question/show на:


public function executeShow()

{

$this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));



$this->forward404Unless($this->question);

}


Добавим в QuestionPeer.php:


public static function getQuestionFromTitle($title)

{

$c = new Criteria();

$c->add(QuestionPeer::STRIPPED_TITLE, $title);



return self::doSelectOne($c);

}


Шаблоны


Вопросы отображаются в question/templates/listSuccess.php. Поместим код шпблона в файл _list.php а содержимое listSuccess.php заменим на:


<h1>popular questions</h1>



<?php echo include_partial('list', array('question_pager' => $question_pager)) ?>

воскресенье, 13 апреля 2008 г.

Вопросы и ответы

propel:

  _attributes:   { noXsd: false, defaultIdMethod: none, package: lib.model }

 

  ask_question:

    _attributes: { phpName: Question, idMethod: native }

    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }

    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }

    title:       { type: longvarchar }

    body:        { type: longvarchar }

    created_at:  ~

    updated_at:  ~

 

  ask_answer:

    _attributes: { phpName: Answer, idMethod: native }

    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }

    question_id: { type: integer, foreignTable: ask_question, foreignReference: id }

    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }

    body:        { type: longvarchar }

    created_at:  ~

 

  ask_user:

    _attributes: { phpName: User, idMethod: native }

    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }

    nickname:    { type: varchar(50), required: true, index: true }

    first_name:  varchar(100)

    last_name:   varchar(100)

    created_at:  ~

 

  ask_interest:

    _attributes: { phpName: Interest, idMethod: native }

    question_id: { type: integer, foreignTable: ask_question, foreignReference: id, primaryKey: true }

    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }

    created_at:  ~

 

  ask_relevancy:

    _attributes: { phpName: Relevancy, idMethod: native }

    answer_id:   { type: integer, foreignTable: ask_answer, foreignReference: id, primaryKey: true }

    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }

    score:       { type: integer }

    created_at:  ~

 

symfony propel-build-model 

symfony propel-build-sql 

mysql -u youruser -p prod < data/sql/lib.model.schema.sql 

symfony propel-insert-sql 

symfony propel-generate-crud frontend question Question 

symfony cc frontend config 

http://prod/question 

 


Шаблон

В apps/frontend/templates/layout.php) меняем:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>

<?php echo include_http_metas() ?>

<?php echo include_metas() ?>

<?php echo include_title() ?>

<link rel="shortcut icon" href="/favicon.ico" />

</head>

<body>

  <div id="header">

    <ul>

      <li><?php echo link_to('about', '@homepage') ?></li>

    </ul>

    <h1><?php echo link_to(image_tag('prod_logo.gif', 'alt=prod'), '@homepage') ?></h1>

  </div>

  <div id="content">

    <div id="content_main">

      <?php echo $sf_data->getRaw('sf_content') ?>

      <div class="verticalalign"></div>

    </div>

    <div id="content_bar">

      <!-- Nothing for the moment -->

      <div class="verticalalign"></div>

    </div>

  </div>

</body>

</html>

Добавим стили в frontend/config/view.yml:

stylesheets:    [main, layout]

Переопределим default homepage


prod/apps/frontend/config/routing.yml:


homepage:

  url:   /

  param: { module: question, action: list }

Определим данные для теста


Создать prod/data/fixtures/

Создать test_data.yml:


User:

  anonymous:

    nickname:   anonymous

    first_name: Anonymous

    last_name:  Coward

  fabien:

    nickname:   fabpot

    first_name: Fabien

    last_name:  Potencier

  francois:

    nickname:   francoisz

    first_name: François

    last_name:  Zaninotto

Question:

  q1:

    title: What shall I do tonight with my girlfriend?

    user_id: fabien

    body:  |

      We shall meet in front of the Dunkin'Donuts before dinner, 

      and I haven't the slightest idea of what I can do with her. 

      She's not interested in programming, space opera movies nor insects.

      She's kinda cute, so I really need to find something 

      that will keep her to my side for another evening.

  q2:

    title: What can I offer to my step mother?

    user_id: anonymous

    body:  |

      My stepmother has everything a stepmother is usually offered

      (watch, vacuum cleaner, earrings, del.icio.us account). 

      Her birthday comes next week, I am broke, and I know that 

      if I don't offer her something sweet, my girlfriend 

      won't look at me in the eyes for another month.

  q3:

    title: How can I generate traffic to my blog?

    user_id: francois

    body:  |

      I have a very swell blog that talks 

      about my class and mates and pets and favorite movies.

Interest:

  i1: { user_id: fabien, question_id: q1 }

  i2: { user_id: francois, question_id: q1 }

  i3: { user_id: francois, question_id: q2 }

  i4: { user_id: fabien, question_id: q2 }

Batch skeleton


Создать файл load_data.php в prod/batch/:


<?php 

define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));

define('SF_APP',         'frontend');

define('SF_ENVIRONMENT', 'dev');

define('SF_DEBUG',       true);

require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');

// initialize database manager 

$databaseManager = new sfDatabaseManager();

$databaseManager->initialize();

?> 

Импорт данных 

Добавить код до конца?> в prod/batch/load_data.php :


$data = new sfPropelData();

$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

$ cd /home/sfprojects/prod/batch

$ php load_data.php

Проверяем:


http://prod/frontend_dev.php

Доступ к данным


Метод executeList() в prod/apps/frontend/modules/question/actions/action.class.php и шаблон

prod/apps/frontend/modules/question/templates/listSuccess.php:

actions.class.php:


public function executeList ()

{ 

  $this->questions = QuestionPeer::doSelect(new Criteria());

} 

listSuccess.php:

...

<?php foreach ($questions as $question): ?>

<tr>

    <td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td>

    <td><?php echo $question->getTitle() ?></td>

    <td><?php echo $question->getBody() ?></td>

    <td><?php echo $question->getCreatedAt() ?></td>

    <td><?php echo $question->getUpdatedAt() ?></td>

  </tr>

<?php endforeach; ?>

Изменим шаблон  question/list

В файле listSuccess.php из prod/apps/frontend/modules/question/templates/ изменим:


<?php use_helper('Text') ?>

<h1>popular questions</h1> 

<?php foreach($questions as $question): ?>

  <div class="question">

    <div class="interested_block">

      <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">

        <?php echo count($question->getInterests()) ?>

      </div>

    </div>

    <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>

    <div class="question_body">

      <?php echo truncate_text($question->getBody(), 200) ?>

    </div>

  </div>

<?php endforeach; ?>

http://prod/frontend_dev.php/

Просмотр


http://prod/frontend_dev.php/question/show/id/2

prod/apps/frontend/modules/question/actions/actions.class.php :


public function executeShow()

 {

   $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id'));

   $this->forward404Unless($this->question);

 }

Модификация showSuccess.php 

frontend/modules/question/templates/showSuccess.php:


<?php use_helper('Date') ?>

<div class="interested_block">

  <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">

    <?php echo count($question->getInterests()) ?>

  </div>

</div>

<h2><?php echo $question->getTitle() ?></h2>

<div class="question_body">

  <?php echo $question->getBody() ?>

</div>

<div id="answers">

<?php foreach ($question->getAnswers() as $answer): ?>

  <div class="answer">

    posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?> 

    on <?php echo format_date($answer->getCreatedAt(), 'p') ?>

    <div>

      <?php echo $answer->getBody() ?>

    </div>

  </div>

<?php endforeach; ?>

</div>

Добавим данные answer и relevancy  в data/fixtures/test_data.yml:

Answer:

  a1_q1:

    question_id: q1

    user_id:     francois

    body:        |

      You can try to read her poetry. Chicks love that kind of things.

  a2_q1:

    question_id: q1

    user_id:     fabien

    body:        |

      Don't bring her to a donuts shop. Ever. Girls don't like to be

      seen eating with their fingers - although it's nice. 

  a3_q2:

    question_id: q2

    user_id:     fabien

    body:        |

      The answer is in the question: buy her a step, so she can 

      get some exercise and be grateful for the weight she will

      lose.

  a4_q3:

    question_id: q3

    user_id:     fabien

    body:        |

Модификация модели

prod/lib/model/User.php:


public function __toString()

{ 

  return $this->getFirstName().' '.$this->getLastName();

} 

prod/apps/frontend/modules/question/templates/showSuccess.php 

posted by <?php echo $answer->getUser() ?> 

файл  showSuccess.php:

<div class="interested_block">

  <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">

    <?php echo count($question->getInterests()) ?>

  </div>

</div>

создадим _interested_user.php файл в prod/apps/frontend/modules/question/templates/:

<div class="interested_mark" id="mark_<?php echo $question->getId() ?>">

  <?php echo count($question->getInterests()) ?>

</div>

Изменим код в  listSuccess.php и showSuccess.php) на:

<div class="interested_block">

  <?php include_partial('interested_user', array('question' => $question)) ?>

</div>

Добавим поле User 
$ symfony propel-build-ысруьф
$ symfony propel-сщтмуке-ньд-ысруьф

В prod/config/schema.xml добавим ask_question :


<column name="interested_users" type="integer" default="0" /> 


propel:
_attributes:
defaultIdMethod: native
noxsd: true
ask_question:
_attributes:
phpName: Question
id:
type: integer
required: true
primaryKey: true
autoIncrement: true
user_id:
type: integer
foreignTable: ask_user
foreignReference: id
title:
type: longvarchar
body:
type: longvarchar
created_at:
type: timestamp
updated_at:
type: timestamp
interested_users:
type: integer
default: 0
stripped_title:
type: varchar
size: 255
_uniques:
unique_stripped_title:
- stripped_title
ask_answer:
_attributes:
phpName: Answer
id:
type: integer
required: true
primaryKey: true
autoIncrement: true
question_id:
type: integer
foreignTable: ask_question
foreignReference: id
user_id:
type: integer
foreignTable: ask_user
foreignReference: id
body:
type: longvarchar
created_at:
type: timestamp
relevancy_up:
type: integer
default: 0
relevancy_down:
type: integer
default: 0
ask_user:
_attributes:
phpName: User
id:
type: integer
required: true
primaryKey: true
autoIncrement: true
nickname:
type: varchar
size: 50
first_name:
type: varchar
size: 100
last_name:
type: varchar
size: 100
created_at:
type: timestamp
ask_interest:
_attributes:
phpName: Interest
question_id:
type: integer
primaryKey: true
foreignTable: ask_question
foreignReference: id
user_id:
type: integer
primaryKey: true
foreignTable: ask_user
foreignReference: id
created_at:
type: timestamp
ask_relevancy:
_attributes:
phpName: Relevancy
answer_id:
type: integer
primaryKey: true
foreignTable: ask_answer
foreignReference: id
user_id:
type: integer
primaryKey: true
foreignTable: ask_user
foreignReference: id
score:
type: integer
created_at:
type: timestamp


Перестроим модель:

$ symfony propel-build-model

$ symfony propel-build-sql

$ mysql -u youruser -p prod < data/sql/lib.model.schema.sql

$ php batch/load_data.php

$ mysql -u youruser -p prod -e "alter table ask_question add interested_users int default '0'"

Модифицируем метод save() объекта Interest


prod/lib/model/Interest.php:


public function save($con = null)

{  

    $ret = parent::save($con);

    // update interested_users in question table

    $question = $this->getQuestion();

    $interested_users = $question->getInterestedUsers();

    $question->setInterestedUsers($interested_users + 1);

    $question->save($con);

    return $ret;

} 

Для зашиты от обновления при транзакциях:

public function save($con = null)

{ 

  $con = Propel::getConnection();

  try

  {

    $con->begin();

    $ret = parent::save($con);

    // update interested_users in question table

    $question = $this->getQuestion();

    $interested_users = $question->getInterestedUsers();

    $question->setInterestedUsers($interested_users + 1);

    $question->save($con);

    $con->commit();

    return $ret;

  }

  catch (Exception $e)

  {

    $con->rollback();

    throw $e;

  }

} 

изменим template

в _interested_user.php заменим:


<?php echo count($question->getInterests()) ?>

на

<?php echo $question->getInterestedUsers() ?>

.


  • Добавим answer в schema.xml


<column name="relevancy_up" type="integer" default="0" /> 

<column name="relevancy_down" type="integer" default="0" /> 

Перестроим модель 

$ symfony propel-build-model

$ symfony propel-build-sql

$ mysql -u youruser -p prod < data/sql/lib.model.schema.sql


  • Заменим метод->save()в классе Relevancy в lib/model/Relevancy.php


public function save($con = null)

{

  $con = Propel::getConnection();

  try

  {

    $con->begin();

    $ret = parent::save();

    // update relevancy in answer table

    $answer = $this->getAnswer();

    if ($this->getScore() == 1)

    {

      $answer->setRelevancyUp($answer->getRelevancyUp() + 1);

    }

    else

    {

      $answer->setRelevancyDown($answer->getRelevancyDown() + 1);

    }

    $answer->save($con);

    $con->commit();

    return $ret;

  }

  catch (Exception $e)

  {

    $con->rollback();

    throw $e;

  }

}

Добавим Answer class в  модель:

public function getRelevancyUpPercent()

{

  $total = $this->getRelevancyUp() + $this->getRelevancyDown();

  return $total ? sprintf('%.0f', $this->getRelevancyUp() * 100 / $total) : 0;

}

public function getRelevancyDownPercent()

{

  $total = $this->getRelevancyUp() + $this->getRelevancyDown();

  return $total ? sprintf('%.0f', $this->getRelevancyDown() * 100 / $total) : 0;

}

Изменим question/templates/showSuccess.php на:

<div id="answers">

<?php foreach ($question->getAnswers() as $answer): ?>

  <div class="answer">

    <?php echo $answer->getRelevancyUpPercent() ?>% UP <?php echo $answer->getRelevancyDownPercent() ?> % DOWN

    posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?> 

    on <?php echo format_date($answer->getCreatedAt(), 'p') ?>

    <div>

      <?php echo $answer->getBody() ?>

    </div>

  </div>

<?php endforeach; ?>

</div>

Добавим данные для тестирования в fixtures

Relevancy:

  rel1:

    answer_id: a1_q1

    user_id:   fabien

    score:     1

  rel2:

    answer_id: a1_q1

    user_id:   francois

    score:     -1

Альтернативная версия заголовка


в schema.xml добавим:


<column name="stripped_title" type="varchar" size="255" /> 

<unique name="unique_stripped_title"> 

  <unique-column name="stripped_title" />

</unique> 

Перестроим модель:

$ symfony propel-build-model

$ symfony propel-build-sql

$ mysql -u youruser -p prod < data/sql/lib.model.schema.sql

Custom class


создаем myTools.class.php в prod/lib/:


<?php 

class myTools

{ 

  public static function stripText($text)

  {

    $text = strtolower($text);

    // strip all non word chars

    $text = preg_replace('/\W/', ' ', $text);

    // replace all white space sections with a dash

    $text = preg_replace('/\ +/', '-', $text);

    // trim dashes

    $text = preg_replace('/\-$/', '', $text);

    $text = preg_replace('/^\-/', '', $text);

    return $text;

  }

} 

в prod/lib/model/Question.php добавим:

public function setTitle($v)

{ 

  parent::setTitle($v);

  $this->setStrippedTitle(myTools::stripText($v));

} 

Перегружаем изменения:

$ symfony cc

$ php batch/load_data.php

Меняем ссылки на действия show


в listSuccess.php строку


<h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>

Меняем на

<h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>

в actions.class.php:

public function executeShow()

{ 

  $c = new Criteria();

  $c->add(QuestionPeer::STRIPPED_TITLE, $this->getRequestParameter('stripped_title'));

  $this->question = QuestionPeer::doSelectOne($c);

  $this->forward404Unless($this->question);

} 

http://prod/frontend_dev.php/

Меняем routing правила


routing.yml в prod/apps/frontend/config/:


question:

  url:   /question/:stripped_title

  param: { module: question, action: show }

http://prod/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend

суббота, 12 апреля 2008 г.

Плагин sfJQueryTabs

= sfJQueryTabs plugin =

== Overview ==


Plugin генерирует динамическую панель управления на основе библиотеки jQuery в виде вкладок (http://stilbuero.de/jquery/tabs_3/).

== Installation ==

Установить plugin:


{{{

$ php symfony plugin-install http://plugins.symfony-project.com/sfJQueryTabsPlugin

}}}


== Configuration ==


* Разрешить plugin `sfJQueryTabs` в `settings.yml`.

`<my_project>/apps/frontend/config/settings.yml`:


all:
.settings:
enabled_modules: [default, sfJQueryTabs]

* Очистить cache


{{{

$ symfony cc

}}}


* Конфигурфция



Скопировать '''jquerytabs.yml''' в: <my_project>/plugins/sfJQueryTabs/config/examples/ to <my_project>/apps/frontend/config/

Отредактировать его
.



* Скопировать css и javascript



Copy or link (if your operation system supports it) <my_project>/plugins/sfJQueryTabsPlugin/web/* to <my_project>/web/sfJQueryTabsPlugin/


== Как вызвать? ==
{{{

http://<my_project>/<myapp>/sfJQueryTabs

}}}


== Contact ==

* Jordi Llonch ( jordi [at] laigu [dot] net ), [http://www.laigu.net]


 

Строим Wiki

Строим Wiki


Установим плагині:


>symfony plugin-install http://plugins.symfony-project.com/sfPropelVersionableBehaviorPlugin

>symfony plugin-install http://plugins.symfony-project.com/sfAdvancedAdminGeneratorPlugin


В конце файла config/propel.ini изменим:


propel.builder.addBehaviors = true


Изменим модель


 



propel:
article:
id: ~
title: varchar(255)
body: longvarchar
version: integer
updated_at: ~

 


>symfony propel-build-all


В конце файла lib/model/Article.php добавим:


sfPropelBehavior::add('Article', array('versionable'));


 


>symfony clear-cache


http://localhost/frontend_dev.php


Построим админку:


> symfony propel-init-admin frontend wiki Article


Генерим форму apps/frontend/modules/wiki/config/generator.yml :



generator:
class: sfAdvancedAdminGenerator
param:
model_class: Article
theme: default

list:
title: List of Articles
sort: title
click_action: show
display: [=title, updated_at]
filters: [title, updated_at]
object_actions:
_show: { name: View Article }
_edit: { name: Edit Article }
show:
actions:
_list: { name: Back to the list }
_edit: { name: Edit Article }
edit:
fields:
updated_at: { type: plain }
version: { type: plain }
body: { params: size=80x15 }
actions:
_save: { name: Save modifications }
_show: { name: View Article }
_list: { name: Back to the list }
_delete: { name: Delete Article }



http://localhost/frontend_dev.php/wiki


wiki сохраняет все версии документа. Для их просмотра необходимо действие apps/frontend/modules/wiki/actions/actions.class.php:

public function executeHistory()

{

$this->article = $this->getArticleOrCreate();

}


И template для истлрии. apps/frontend/modules/wiki/templates/historySuccess.php:

<?php use_stylesheet('/sf/sf_admin/css/main') ?>


<div id="sf_admin_container">

<h1><?php echo sprintf('History of "%s" modifications', $article->getTitle()) ?></h1>

<div id="sf_admin_content">


<?php foreach ($article->getAllResourceVersions('desc') as $resourceVersion): ?>

<div class="form-row">

<?php echo sprintf("'%s', Version %d, updated on %s (%s)\n",

link_to($resourceVersion->getTitle(), 'wiki/show?id='.$article->getId().'&version='.$resourceVersion->getNumber()),

$resourceVersion->getNumber(),

$resourceVersion->getCreatedAt(),

$resourceVersion->getComment()

) ?>

</div>

<?php endforeach; ?>


<ul class="sf_admin_actions">

<li><?php echo button_to('Show Article', 'wiki/show?id='.$article->getId(), 'class=sf_admin_action_show') ?></li>

<li><?php echo button_to('Edit Article', 'wiki/edit?id='.$article->getId(), 'class=sf_admin_action_edit') ?></li>

</ul>

</div>

</div>


 


http://localhost/frontend_dev.php/wiki/history/id/1


Перепишем класс действий

protected function getArticleOrCreate($id = 'id')

{

$article = parent::getArticleOrCreate($id);

if($this->getRequest()->hasParameter('version'))

{

$article->toVersion($this->getRequest()->getParameter('version'));

}


return $article;

}


 


В форме добавим поле:


_history: { name: History }

суббота, 5 апреля 2008 г.

Страница новостей (продолжение)

Формируем глпавный шаблон проекта



<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>



<?php include_http_metas() ?>

<?php include_metas() ?>



<?php include_title() ?>



<link rel="shortcut icon" href="/favicon.ico" />







</head>

<body>



<div id="container">




<div id="header">

<div id="banner">


&nbsp;


</div>


<div id="logo">


&nbsp;


</div>


</div>



<div id="topmenu">



&nbsp;



</div>



<div id="navigation">





Extra Content

<fieldset>


<legend>Collapsible List &mdash; Take 4</legend>


<ul>

<li>Item 1</li>




<li>Item 2</li>


<li>


Item 3


<ul>


<li>Item 3.1</li>


<li>


Item 3.2

<ul>


<li>Item 3.2.1</li>




<li>Item 3.2.2</li>


<li>Item 3.2.3</li>


</ul>


</li>

<li>Item 3.3</li>


</ul>


</li>




<li>


Item 4


<ul>


<li>Item 4.1</li>


<li>

Item 4.2


<ul>


<li>Item 4.2.1</li>


<li>Item 4.2.2</li>




</ul>


</li>


</ul>

</li>


<li>Item 5</li>


</ul>


</fieldset>



</div>



<div id="content">



Primary Content





<div id="smallFeatures">

<div id="smallFeaturesInner">




<div id="feature2" class="feature">


<div id="feature2Inner">


<h3>

Secondary title I


</h3>





<p>


Какой-то текст.


</p>

<p class="featureContinue">


<a href="#">Visit the page</a>




</p>


</div>

</div>

<!-- END feature2 -->


<div id="feature3" class="feature">


<div id="feature3Inner">


<h3>

Secondary title II


</h3>




<p>


Какой-то текст.


</p>


<p class="featureContinue">

<a href="#">Visit the page</a>


</p>


</div>

<!-- END feature3Inner -->


</div>

<!-- END feature3 -->


<div id="feature4" class="feature">




<div id="feature4Inner">


<h3>


Secondary title III

</h3>




<p>


Какой-то текст.


</p>


<p class="featureContinue">

<a href="#">Visit the page</a>


</p>


</div>

<!-- END feature4Inner -->


</div>

<!-- END feature4 -->


<div class="clearer">


</div>


</div>

<!-- END smallFeaturesInner -->




<div id="feature1">







<?php echo $sf_data->getRaw('sf_content') ?>





<p class="featureContinue">


<?php echo link_to('back to the news list', 'public/index',

'style=display:block;float:right;') ?>


</p>





</div>

<!-- END feature1 -->


</div>

<!-- END smallFeatures -->


<div class="clearer">

</div>


</div>

<!-- END content -->



</div>



<div id="footer">

Footer<p>Force Layout:

</p>

</div>

</div>



</body>

</html>




Навигационную панель слева строим с помощью jquery

Пишем скрипт menu и подключаем его и jquery к проекту



menu


$(function(){


$('li:has(ul)')


.click(function(event){

if (this == event.target) {


if ($(this).children().is(':hidden')) {


$(this)


.css('list-style-image','url(minus.gif)')


.children().slideDown();

}


else {


$(this)


.css('list-style-image','url(plus.gif)')


.children().slideUp();


}


}

return false;


})


.css({cursor:'pointer',


'list-style-image':'url(plus.gif)'})


.children().hide();


$('li:not(:has(ul))').css({

cursor: 'default',


'list-style-image':'none'


});


});





default:


http_metas:


content-type: text/html




metas:


title: symfony project

robots: index, follow


description: symfony project


keywords: symfony, project


language: en




stylesheets: [main, art]




javascripts: [jquery, menu]




has_layout: on

layout: layout







Чтобы все это работало, добавляем нужные стили



body

{


text-align:center;


color:#333;


font-family: sans-serif;


margin:0px 0;


background-color: #FBFFC0;

}



body,td {


font-size: 10pt;


}

table {


background-color: black;


border: 1px black solid;


border-collapse: collapse;


}

th {


border: 1px outset silver;

background-color: maroon;


color: white;


}

tr {


background-color: white;


margin: 1px;


}

tr.striped {


background-color: coral;


}

td {

padding: 1px 8px;


}



#jQueryStatement {


color: maroon;

}



fieldset {width: auto;


margin-bottom: 12px;


border-color: #00457b;


background-color: #cfeace;

}



fieldset div {

margin-bottom: 6px;


font-weight: normal;

}



legend {


border: 2px ridge #00457b;


font-weight: bold;


background-color: #e36a51;


color: white;


padding: 2px 16px;

}



button {

background-color: #2a523c;


padding: 2px 16px;


color: #cfeace;


font-weight:bold;

}



a,#exampleIndex li {


color: #2a523c;

}



a:hover,#exampleIndex li:hover {


color: #ff0088;

}



#exampleIndex ul {


list-style: none;


margin: 8px;


padding: 0;

}



#exampleIndex li {


text-decoration: underline;


font-weight: bold;


cursor: pointer;


margin: 0;


padding: 0;

}



.chapterBlock {


background-color: #ddeadc;


border: 3px double #00457b;


margin: 6px 8px;


padding: 6px;

}



p{

font-size:.9em;

line-height:1.3em;

margin:20px auto;

}

#notice p{

text-align:justify !important;

}

#container{

width:100%;

margin:0 auto;

}

#container p{

text-align:right;

}

#header{

background-color: #FFA345;

width:100%;

line-height:90px;

color:#fff;

}



#header #banner {

background-color: #FFA345;

width:80%;

line-height:90px;

color:#fff;

float:left;

}

#header #logo{

background-color: #FF0045;

width:20%;

color:#fff;

float:right;

}



#header #mbanner{

background-color: #FFA345;

width:100%;

line-height:90px;

color:#fff;

float:right;

}



#content{

background-color: #00009F;

width:80%;

float:right;

color:#fff;

}



#topmenu{

background-color: #94C6FF;

width:100%;

line-height:35px;

clear:both;

}

#topmenu #calendar{

background:url(../images/top_back1.jpg) no-repeat;

width:395px;

line-height:35px;

float:left;

}



#topmenu #topm{

background:url(../images/top_back1.jpg);

width:592px;

line-height:35px;

float:right;

}

#navigation{

background-color: #C5FF8A;

width:250px;

float:left;

}





#extraContent{

display:none;

}

#footer{

background-color: #94C6FF;

width:100%;

line-height:50px;

clear:both;

}





a{

font-weight:bold;

color:#000;

cursor: pointer;

}

.article

{

color:#666;

float:left;

}



#smallFeatures

{


position: static;


border: none;


border-bottom: 1px solid #CCCCCC;


padding: 0;

}



#smallFeatures .feature

{


clear: none;


float: left;


width: 33.3%;


border-top: 0 none #FFFFFF;


padding: 0;


background-color: transparent;

}



#smallFeatures .feature h3

{


clear: none;

margin: 0 0 15px 0;


border-top: 1px solid #CCCCCC;


border-bottom: 1px solid #CCCCCC;


padding: 8px 15px;


background-color: #EEEEEE;


background-image: none;

}

#feature1{


margin: 0 15px 15px 15px;

}

#smallFeatures .feature p

{


margin: 0 15px 15px 15px;

}



#smallFeatures .featureContinue

{


border-top: 1px dashed #CCCCCC;


padding-top: 0.25em;

}







В шаблоне index для чтения новости целиком создаем ссылку read more...

<div id="main">


<h1>News and articles</h1>




<?php foreach($articles as $article): ?>







<fieldset>


<legend><?php echo $article->getCreatedAt('d/M/Y') ?></legend>

<div class="article">




<?php echo link_to(


image_tag('/uploads/'.$article->getFilePath()),


'public/article?id='.$article->getId(),


'class=image title='.$article->getTitle()

) ?>




<?php echo $article->getTeaser() ?>





<?php echo link_to('read more... ', 'public/article?id='.$article->getId(),


'style=display:block;float:right;') ?>

</div>


</fieldset>


<?php endforeach; ?>




<div id="footer">


powered by <?php echo link_to('symfony', 'http://www.symfony-project.com') ?>

</div>



</div>



И соответствующий этой ссылке action в классе publicActions

<?php



/**


* public actions.


*


* @package prod


* @subpackage public


* @author Your name here

* @version SVN: $Id: actions.class.php 2692 2006-11-15 21:03:55Z fabien $


*/

class publicActions extends sfActions

{


/**


* Executes index action


*


*/


public function executeIndex()

{


//$this->forward('default', 'module');


$c = new Criteria();


$c->addDescendingOrderByColumn(ArticlePeer::CREATED_AT);


$this->articles = ArticlePeer::doSelect($c);




}


public function executeArticle()




{


$this->article = ArticlePeer::retrieveByPk($this->getRequestParameter('id'));


$this->forward404unless($this->article);

$c = new Criteria();


$c->add(ArticlePeer::ID, $this->getRequestParameter('id'));


$this->articles = ArticlePeer::doSelect($c);





}

}



А для представления - шаблон














<h1><?php echo $article->getTitle() ?> </h1>

<fieldset>


<legend><?php echo $article->getCreatedAt('d/M/Y') ?></legend>







<p>


<?php echo link_to(


image_tag('/uploads/'.$article->getFilePath()),


'public/article?id='.$article->getId(),


'class=image title='.$article->getTitle()

) ?>




<?php echo $article->getBody() ?>


</p>

</fieldset>