merge from 1.0

This commit is contained in:
qiang.xue
2009-02-14 04:27:48 +00:00
parent 876428500e
commit 1d21d29347
79 changed files with 3017 additions and 204 deletions

View File

@@ -13,9 +13,18 @@ Version 1.0.3 to be released
- Bug: CHttpRequest.hostInfo may give wrong port number (Qiang)
- Bug: CHtml::activeListBox does not work when multiple selection is needed (Qiang)
- Bug: Inconsistency in timezone of log messages for different log routes (Qiang)
- Bug: Script file registered for POS_BEGIN is rendered twice (Qiang)
- New #117: Added count() support to relational AR (Qiang)
- New #136: Added support to CWebUser to allow directly accessing persistent properties (Qiang)
- New: Upgraded jquery to 1.3.1 (Qiang)
- New: Upgraded jquery star rating to 2.61 (Qiang)
- New: Added skeleton application and refactored 'yiic webapp' command (Qiang)
- New: Added 'expression' option to access rules (Qiang)
- New: Refactored the code generated by yiic command (Qiang)
- New: Refactored the blog demo (Qiang)
- New: Added ignoreLimit option when joining tables all at once (Qiang)
- New: Added params option to relation declaration (Qiang)
- New: Added the blog tutorial (Qiang)
Version 1.0.2 February 1, 2009

View File

@@ -121,11 +121,17 @@
<echo>Building Guide PDF...</echo>
<exec command="${build} guideLatex" dir="." passthru="true" />
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guideLatex/output" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guideLatex/output" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guideLatex/output" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<move file="commands/guide/guide.pdf" tofile="${build.doc.dir}/yii-guide-${yii.version}.pdf" />
<move file="commands/guideLatex/output/guide.pdf" tofile="${build.doc.dir}/yii-guide-${yii.version}.pdf" />
<echo>Building Blog PDF...</echo>
<exec command="${build} blogLatex" dir="." passthru="true" />
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<move file="commands/blog/blog.pdf" tofile="${build.doc.dir}/yii-blog-${yii.version}.pdf" />
<echo>Building API...</echo>
<exec command="${build} api ${build.doc.dir}" dir="." passthru="true" />

View File

@@ -0,0 +1,79 @@
<?php
class BlogLatexCommand extends CConsoleCommand
{
function getHelp()
{
return <<<EOD
USAGE
yiic bloglatex
DESCRIPTION
This command generates latex files for the definitive guide.
The generated latex files are stored in the blog directory.
EOD;
}
function getSourceDir()
{
return dirname(__FILE__).'/../../docs/blog';
}
function getOutputDir()
{
return dirname(__FILE__).'/blog';
}
function run($args)
{
require_once(dirname(__FILE__).'/markdown/MarkdownHtml2Tex.php');
$sourcePath=$this->getSourceDir();
$chapters=$this->getTopics();
$toc = '';
foreach($chapters as $chapter=>$sections)
{
$toc .= sprintf("\chapter{%s}\n", $chapter);
foreach($sections as $path=>$section)
{
echo "creating '$section'...";
$content=file_get_contents($sourcePath."/{$path}.txt");
$this->createLatexFile($chapter,$section,$content, $path);
echo "done\n";
$toc .= sprintf("\input{%s}\n", $path);
}
}
$main_file = sprintf('%s/main.tex', $this->getOutputDir());
file_put_contents($main_file, $toc);
}
function getTopics()
{
$file = $this->getSourceDir().'/toc.txt';
$lines=file($file);
$chapter='';
$guideTopics=array();
foreach($lines as $line)
{
if(($line=trim($line))==='')
continue;
if($line[0]==='*')
$chapter=trim($line,'* ');
else if($line[0]==='-' && preg_match('/\[(.*?)\]\((.*?)\)/',$line,$matches))
$guideTopics[$chapter][$matches[2]]=$matches[1];
}
return $guideTopics;
}
function createLatexFile($chapter, $section, $content, $path)
{
$parser=new MarkdownParserLatex;
$content=$parser->transform($content);
$img_src = $this->getSourceDir().'/images';
$img_dst = $this->getOutputDir();
$html2tex = new MarkdownHtml2Tex($img_src, $img_dst);
$tex = $html2tex->parse_html($content, $path);
$filename = sprintf('%s/%s.tex', $this->getOutputDir(), $path);
file_put_contents($filename, $tex);
}
}

View File

@@ -10,26 +10,25 @@ USAGE
DESCRIPTION
This command generates latex files for the definitive guide.
The generated latex files are stored in following directory:
guideLatex/output
The generated latex files are stored in the guide directory.
EOD;
}
function getGuideSourceDir()
function getSourceDir()
{
return dirname(__FILE__).'/../../docs/guide';
}
function getOutputDir()
{
return dirname(__FILE__).'/guideLatex/output';
return dirname(__FILE__).'/guide';
}
function run($args)
{
require_once(dirname(__FILE__).'/guideLatex/MarkdownHtml2Tex.php');
$sourcePath=$this->getGuideSourceDir();
require_once(dirname(__FILE__).'/markdown/MarkdownHtml2Tex.php');
$sourcePath=$this->getSourceDir();
$chapters=$this->getGuideTopics();
$toc = '';
foreach($chapters as $chapter=>$sections)
@@ -50,7 +49,7 @@ EOD;
function getGuideTopics()
{
$file = $this->getGuideSourceDir().'/toc.txt';
$file = $this->getSourceDir().'/toc.txt';
$lines=file($file);
$chapter='';
$guideTopics=array();
@@ -70,7 +69,7 @@ EOD;
{
$parser=new MarkdownParserLatex;
$content=$parser->transform($content);
$img_src = $this->getGuideSourceDir().'/images';
$img_src = $this->getSourceDir().'/images';
$img_dst = $this->getOutputDir();
$html2tex = new MarkdownHtml2Tex($img_src, $img_dst);
$tex = $html2tex->parse_html($content, $path);

View File

@@ -1,37 +1,37 @@
\chapter*{License of Yii}
\addcontentsline{toc}{chapter}{License}
The Yii framework is free software.
The Yii framework is free software.
It is released under the terms of the following BSD License.
Copyright \copyright 2008 by Yii Software LLC.
Copyright \copyright 2008-2009 by Yii Software LLC.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
\begin{enumerate}
\item Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
\item Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
\item Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
\item Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
\item Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
\end{enumerate}
{\footnotesize
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
}

View File

@@ -0,0 +1,25 @@
%Title Page:
%\renewcommand{\baselinestretch}{1}
\thispagestyle{empty}
\begin{center}
\bfseries \rule{0cm}{1.5cm} \Huge
Building a Blog System using Yii
\vspace{1.5cm}
\end{center}
\begin{center}
\bfseries \Large Qiang Xue
\end{center}
%\begin{center} \large \today \end{center}
\begin{center}
Copyright 2008-2009. All Rights Reserved.
\end{center}
\vfill
%Take a blank Page
\pagebreak \thispagestyle{empty} \cleardoublepage
%\renewcommand{\baselinestretch}{1.1}

View File

@@ -0,0 +1,30 @@
\documentclass[a4paper,11pt,twoside]{book}
\input{preamble} % configurations
\begin{document}
\frontmatter
\include{title} % include title page
\addcontentsline{toc}{chapter}{Contents}
\pagenumbering{roman}
\tableofcontents
\include{preface} % include preface
\include{license} % include other frontmatter
\mainmatter
\input{main} % include chapter list
%\appendix
%\include{history}
%\backmatter
%\include{bibliography} % include bibliography
%\include{index} % include index
\end{document}

View File

@@ -0,0 +1,37 @@
\chapter*{License of Yii}
\addcontentsline{toc}{chapter}{License}
The Yii framework is free software.
It is released under the terms of the following BSD License.
Copyright \copyright 2008-2009 by Yii Software LLC.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
\begin{enumerate}
\item Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
\item Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
\item Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
\end{enumerate}
{\footnotesize
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
}

View File

@@ -0,0 +1,144 @@
\usepackage{xspace, calc, enumerate, fancyhdr, alltt, color, ifthen, hyphenat}
\usepackage[pdftex]{graphicx}
\usepackage[pdftex]{hyperref}
%----- remove hyphen character for texttt ----
\renewcommand{\BreakableUnderscore}{\nobreak\textunderscore\discretionary{}{}{}\nobreak}
\renewcommand{\BreakableBackslash}{\nobreak\textbackslash\discretionary{}{}{}\nobreak}
\renewcommand{\BreakableSlash}{\nobreak/\discretionary{}{}{}\nobreak}
\renewcommand{\BreakablePeriod}{\nobreak.\discretionary{}{}{}\nobreak}
\renewcommand{\BreakableColon}{\nobreak:\discretionary{}{}{}\nobreak}
\definecolor{lightp}{rgb}{0.4,0.2,0.8}
%------ syntax highlighting colors --------
\definecolor{string}{rgb}{1,0.0,0.53}
\definecolor{comment}{rgb}{0.2,0.6,0.0}
\definecolor{keyword}{rgb}{0.0,0.3,1.0}
\definecolor{reserved}{rgb}{0.0,0.3,1.0}
\definecolor{identifier}{rgb}{0.0,0.0,0.0}
\definecolor{brackets}{rgb}{0.60,0.80,0.0}
\definecolor{code}{rgb}{0.0,0.0,0.0}
\definecolor{number}{rgb}{0.93,0.0,0.0}
\definecolor{inlinetags}{rgb}{0.0,0.0,1.0}
\definecolor{quotes}{rgb}{1,0.0,0.53}
\definecolor{var}{rgb}{0.0,0.66,0.66}
\definecolor{inlinedoc}{rgb}{0.0,0.66,0.66}
\definecolor{default}{rgb}{0.0,0.0,0.0}
%---- change link style ---- See http://andy-roberts.net/misc/latex/pdftutorial.html
\hypersetup{colorlinks, urlcolor=lightp, linkcolor=blue, bookmarksopen=true, pdfstartview={FitH}}
%------------------------Page set-up-----------------------------------------
\renewcommand{\baselinestretch}{1.15} % line spacing 115%
\setlength{\hoffset}{-1in}
\setlength{\oddsidemargin}{4cm}
\setlength{\evensidemargin}{2cm}
\setlength{\voffset}{3cm - 1in}
\setlength{\topmargin}{0cm}
\setlength{\headheight}{14.5pt}
%\setlength{\headsep}{0cm}
\setlength{\marginparwidth}{0cm}
\setlength{\marginparsep}{0cm}
\setlength{\marginparpush}{0cm}
%\setlength{\footskip}{0cm}
\setlength{\textheight}{242mm - \headheight - \headsep - \footskip}
\setlength{\textwidth}{15cm}
\setlength{\parindent}{0cm}
\setlength{\parskip}{0.75\baselineskip}
%------------------------------------------------------------------------------
%------------------------Setup headings and spacing---------------------
\makeatletter
\renewcommand{\section}{\@startsection%
{section}% name
{1}% level
{0mm}% indent
{0.5\baselineskip}% beforeskip
{0.1mm}% afterskip
{\normalfont\Large\bfseries}% style
}
\renewcommand{\subsection}{\@startsection%
{subsection}% name
{2}% level
{0mm}% indent
{0.5\baselineskip}% beforeskip
{0.1mm}% afterskip
{\normalfont\large\bfseries}% style
}
\makeatother
%------------------------------------------------------------------------------
% Modify the first page of the chapters: no chapter word, big chapter
% number, title adjusted to right, and a line below.
%-----------------------------------------------------------------------
\makeatletter
\def\@makechapterhead#1{%
% {\parindent \z@\raggedleft
{\parindent \z@\raggedright
\ifnum \c@secnumdepth >\m@ne
{\fontsize{20pt}{30pt}\selectfont\Huge\textsc{{Chapter \thechapter}}}
\fi
\rule{\columnwidth}{0.25pt} \par
{\Huge\textsc{\textbf{#1}}\par}
\nobreak
\vskip 70\p@
}}
\def\@makeschapterhead#1{%
% {\parindent\z@\raggedleft
{\parindent\z@\raggedright
{\Huge\textsc{#1}\par}
\nobreak
\vskip 70\p@
}}
\makeatother
%-----------------------------------------------------------------------
% Modify the caption definition to make text italic and smaller than normal size text
%-----------------------------------------------------------------------
\makeatletter
\newsavebox{\capbox}
\renewcommand{\@makecaption}[2]{%
\vspace{0pt}\sbox{\capbox}{\textbf{#1}: \small{#2}}%
\ifthenelse{\lengthtest{\wd\capbox > 0.85\linewidth}}%
{\center{\parbox[t]{0.8\linewidth}{\textbf{#1}: \small{#2}}}}%
{\begin{center}\textbf{#1}: \small{#2}\end{center}}%
} \makeatother
%-----------------------------------------------------------------------
% Pages and Fancyheadings stuff
%-----------------------------------------------------------------------
\pagestyle{fancy}
% \pagestyle{fancyplain}
%\setlength{\headrulewidth}{0.1pt} \setlength{\footrulewidth}{0pt}
\newcommand{\clearemptydoublepage}{\newpage\thispagestyle{empty}\cleardoublepage}
\cfoot{}
% Chapter
\renewcommand{\chaptermark}[1]{\markboth{\thechapter.\ #1}{}}
% % Section
\renewcommand{\sectionmark}[1]{\markright{\thesection\ #1}}
\fancyhf{}
\fancyhead[LE,RO]{\bfseries\thepage}
\fancyhead[LO]{\nouppercase{\bfseries\rightmark}}
\fancyhead[RE]{\nouppercase{\scshape\leftmark}}
% \lhead[\fancyplain{}{\bf\thepage}]{\fancyplain{}{\bf\rightmark}}
% \rhead[\fancyplain{}{\bf\leftmark}]{\fancyplain{}{\bf\thepage}}
\fancypagestyle{plain}{%
\fancyhf{} % clear all header and footer fields
\renewcommand{\headrulewidth}{0pt}
\renewcommand{\footrulewidth}{0pt}}
%-----------------------------------------------------------------------
% boxes
\newsavebox{\fmboxb}
\newenvironment{tipbox}
{\vspace{-2mm}\begin{center}\begin{lrbox}{\fmboxb}\hspace{2mm}
\begin{minipage}{0.85\textwidth} \vspace{1mm}\small\setlength{\parskip}{0.75\baselineskip}}
{ \vspace{2mm} \end{minipage}
\hspace{2mm}\end{lrbox}\fbox{\usebox{\fmboxb}}\end{center}}

View File

@@ -6,13 +6,13 @@ class UserLogin extends Portlet
protected function renderContent()
{
$user=new LoginForm;
$form=new LoginForm;
if(isset($_POST['LoginForm']))
{
$user->attributes=$_POST['LoginForm'];
if($user->validate())
$form->attributes=$_POST['LoginForm'];
if($form->validate())
$this->controller->refresh();
}
$this->render('userLogin',array('user'=>$user));
$this->render('userLogin',array('form'=>$form));
}
}

View File

@@ -1,19 +1,19 @@
<?php echo CHtml::form(); ?>
<div class="row">
<?php echo CHtml::activeLabel($user,'username'); ?>
<?php echo CHtml::activeLabel($form,'username'); ?>
<br/>
<?php echo CHtml::activeTextField($user,'username') ?>
<?php echo CHtml::error($user,'username'); ?>
<?php echo CHtml::activeTextField($form,'username') ?>
<?php echo CHtml::error($form,'username'); ?>
</div>
<div class="row">
<?php echo CHtml::activeLabel($user,'password'); ?>
<?php echo CHtml::activeLabel($form,'password'); ?>
<br/>
<?php echo CHtml::activePasswordField($user,'password') ?>
<?php echo CHtml::error($user,'password'); ?>
<?php echo CHtml::activePasswordField($form,'password') ?>
<?php echo CHtml::error($form,'password'); ?>
</div>
<div class="row">
<?php echo CHtml::activeCheckBox($user,'rememberMe'); ?>
<?php echo CHtml::label('Remember me next time',CHtml::getActiveId($user,'rememberMe')); ?>
<?php echo CHtml::activeCheckBox($form,'rememberMe'); ?>
<?php echo CHtml::activeLabel($form,'rememberMe'); ?>
</div>
<div class="row">
<?php echo CHtml::submitButton('Login'); ?>

View File

@@ -19,11 +19,6 @@ class CommentController extends CController
{
const PAGE_SIZE=10;
/**
* @var string specifies the default action to be 'admin'.
*/
public $defaultAction='admin';
/**
* @var CActiveRecord the currently loaded data model instance.
*/
@@ -98,7 +93,6 @@ class CommentController extends CController
{
if(Yii::app()->request->isPostRequest)
{
// we only allow deletion via POST request
$comment=$this->loadComment();
$comment->approve();
$this->redirect(array('post/show','id'=>$comment->postId,'#'=>'c'.$comment->id));

View File

@@ -147,19 +147,20 @@ class PostController extends CController
$criteria=new CDbCriteria;
$criteria->condition='status='.Post::STATUS_PUBLISHED;
$criteria->order='createTime DESC';
$withOption=array('author');
if(!empty($_GET['tag']))
{
$criteria->join='JOIN PostTag ON PostTag.postId=Post.id JOIN Tag ON PostTag.tagId=Tag.id';
$criteria->condition.=' AND Tag.name=:tag';
$criteria->params[':tag']=$_GET['tag'];
$withOption['tagFilter']['params'][':tag']=$_GET['tag'];
$postCount=Post::model()->with($withOption)->count($criteria);
}
else
$postCount=Post::model()->count($criteria);
$pages=new CPagination(Post::model()->count($criteria));
$pages=new CPagination($postCount);
$pages->pageSize=Yii::app()->params['postsPerPage'];
$pages->applyLimit($criteria);
$posts=Post::model()->with('author')->findAll($criteria);
$posts=Post::model()->with($withOption)->together(true)->findAll($criteria);
$this->render('list',array(
'posts'=>$posts,
'pages'=>$pages,
@@ -197,13 +198,9 @@ class PostController extends CController
*/
public function getTagLinks($post)
{
$tags=$post->tags;
$links=array();
foreach($tags as $tag)
{
$url=$this->createUrl('list',array('tag'=>$tag->name));
$links[]=CHtml::link(CHtml::encode($tag->name),$url);
}
foreach($post->getTagArray() as $tag)
$links[]=CHtml::link(CHtml::encode($tag),array('list','tag'=>$tag));
return implode(', ',$links);
}

Binary file not shown.

View File

@@ -13,6 +13,7 @@ CREATE TABLE Post
title VARCHAR(128) NOT NULL,
content TEXT NOT NULL,
contentDisplay TEXT,
tags TEXT,
status INTEGER NOT NULL,
createTime INTEGER,
updateTime INTEGER,
@@ -55,11 +56,11 @@ CREATE TABLE PostTag
);
INSERT INTO User (username, password, email) VALUES ('demo','fe01ce2a7fbac8fafaed7c982a04e229','webmaster@example.com');
INSERT INTO Post (title, content, contentDisplay, status, createTime, updateTime, authorId) VALUES ('Welcome to Yii Blog','This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.
INSERT INTO Post (title, content, contentDisplay, status, createTime, updateTime, authorId, tags) VALUES ('Welcome to Yii Blog','This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.
Feel free to try this system by writing new posts and posting comments.','<p>This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.</p>
<p>Feel free to try this system by writing new posts and posting comments.</p>',1,1230952187,1230952187,1);
<p>Feel free to try this system by writing new posts and posting comments.</p>',1,1230952187,1230952187,1,'yii, blog');
INSERT INTO Tag (name) VALUES ('yii');

View File

@@ -13,6 +13,7 @@ CREATE TABLE Post
title VARCHAR(128) NOT NULL,
content TEXT NOT NULL,
contentDisplay TEXT,
tags TEXT,
status INTEGER NOT NULL,
createTime INTEGER,
updateTime INTEGER,
@@ -55,11 +56,11 @@ CREATE TABLE PostTag
);
INSERT INTO User (username, password, email) VALUES ('demo','fe01ce2a7fbac8fafaed7c982a04e229','webmaster@example.com');
INSERT INTO Post (title, content, contentDisplay, status, createTime, updateTime, authorId) VALUES ('Welcome to Yii Blog','This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.
INSERT INTO Post (title, content, contentDisplay, status, createTime, updateTime, authorId, tags) VALUES ('Welcome to Yii Blog','This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.
Feel free to try this system by writing new posts and posting comments.','<p>This blog system is developed using Yii. It is meant to demonstrate how to use Yii to build a complete real-world application. Complete source code may be found in the Yii releases.</p>
<p>Feel free to try this system by writing new posts and posting comments.</p>',1,1230952187,1230952187,1);
<p>Feel free to try this system by writing new posts and posting comments.</p>',1,1230952187,1230952187,1,'yii, blog');
INSERT INTO Tag (name) VALUES ('yii');
INSERT INTO Tag (name) VALUES ('blog');

View File

@@ -68,7 +68,13 @@ class Comment extends CActiveRecord
*/
public function safeAttributes()
{
return 'author, email, url, content, verifyCode';
return array(
'author',
'email',
'url',
'content',
'verifyCode',
);
}
/**

View File

@@ -26,6 +26,18 @@ class LoginForm extends CFormModel
);
}
/**
* Declares the attribute labels.
* If an attribute is not delcared here, it will use the default label
* generation algorithm to get its label.
*/
public function attributeLabels()
{
return array(
'rememberMe'=>'Remember me next time',
);
}
/**
* Authenticates the password.
* This is the 'authenticate' validator as declared in rules().

View File

@@ -5,10 +5,6 @@ class Post extends CActiveRecord
const STATUS_DRAFT=0;
const STATUS_PUBLISHED=1;
const STATUS_ARCHIVED=2;
/**
* @var string this property is used to collect user tag input
*/
public $tagInput;
/**
* Returns the static model of the specified AR class.
@@ -33,10 +29,10 @@ class Post extends CActiveRecord
public function rules()
{
return array(
array('title', 'length', 'max'=>128),
array('title, content, status', 'required'),
array('status', 'numerical', 'min'=>0, 'max'=>3),
array('tagInput', 'match', 'pattern'=>'/[\w\s,]+/', 'message'=>'Tags can only contain word characters.'),
array('title', 'length', 'max'=>128),
array('status', 'in', 'range'=>array(0, 1, 2)),
array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/', 'message'=>'Tags can only contain word characters.'),
);
}
@@ -45,7 +41,12 @@ class Post extends CActiveRecord
*/
public function safeAttributes()
{
return 'title, content, status, tagInput';
return array(
'title',
'content',
'status',
'tags',
);
}
/**
@@ -56,18 +57,16 @@ class Post extends CActiveRecord
return array(
'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
'comments'=>array(self::HAS_MANY, 'Comment', 'postId', 'order'=>'??.createTime'),
'tags'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)'),
'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)', 'joinType'=>'INNER JOIN', 'condition'=>'??.name=:tag'),
);
}
/**
* @return array customized attribute labels (name=>label)
* @return array tags
*/
public function attributeLabels()
public function getTagArray()
{
return array(
'tagInput'=>'Tags',
);
return array_unique(preg_split('/\s*,\s*/',trim($this->tags),-1,PREG_SPLIT_NO_EMPTY));
}
/**
@@ -108,15 +107,6 @@ class Post extends CActiveRecord
return true;
}
protected function afterFind()
{
$tags=$this->tags;
$tagInputs=array();
foreach($tags as $tag)
$tagInputs[]=$tag->name;
$this->tagInput=implode(', ',$tagInputs);
}
/**
* Postprocessing after the record is saved
*/
@@ -125,8 +115,7 @@ class Post extends CActiveRecord
if(!$this->isNewRecord)
$this->dbConnection->createCommand('DELETE FROM PostTag WHERE postId='.$this->id)->execute();
$tags=array_unique(preg_split('/\s*,\s*/',trim($this->tagInput),-1,PREG_SPLIT_NO_EMPTY));
foreach($tags as $name)
foreach($this->getTagArray() as $name)
{
if(($tag=Tag::model()->findByAttributes(array('name'=>$name)))===null)
{

View File

@@ -45,7 +45,7 @@ You may use <a href="http://daringfireball.net/projects/markdown/syntax" target=
<?php endif; ?>
<div class="row action">
<?php echo CHtml::submitButton($buttonLabel,array('name'=>'submitComment')); ?>
<?php echo CHtml::submitButton($update ? 'Save' : 'Submit', array('name'=>'submitComment')); ?>
<?php echo CHtml::submitButton('Preview',array('name'=>'previewComment')); ?>
</div>

View File

@@ -2,5 +2,5 @@
<?php $this->renderPartial('_form', array(
'comment'=>$comment,
'buttonLabel'=>'Save',
'update'=>true,
)); ?>

View File

@@ -15,8 +15,8 @@ You may use <a href="http://daringfireball.net/projects/markdown/syntax" target=
</p>
</div>
<div class="row">
<?php echo CHtml::activeLabel($post,'tagInput'); ?>
<?php echo CHtml::activeTextField($post,'tagInput',array('size'=>65)); ?>
<?php echo CHtml::activeLabel($post,'tags'); ?>
<?php echo CHtml::activeTextField($post,'tags',array('size'=>65)); ?>
<p class="hint">
Separate different tags with commas.
</p>
@@ -27,7 +27,7 @@ Separate different tags with commas.
</div>
<div class="row action">
<?php echo CHtml::submitButton($buttonLabel,array('name'=>'submitPost')); ?>
<?php echo CHtml::submitButton($update ? 'Save' : 'Create', array('name'=>'submitPost')); ?>
<?php echo CHtml::submitButton('Preview',array('name'=>'previewPost')); ?>
</div>

View File

@@ -2,5 +2,5 @@
<?php $this->renderPartial('_form', array(
'post'=>$post,
'buttonLabel'=>'Create',
'update'=>false,
)); ?>

View File

@@ -19,7 +19,7 @@
<?php $this->renderPartial('/comment/_form',array(
'comment'=>$newComment,
'buttonLabel'=>'Submit',
'update'=>false,
)); ?>
</div><!-- comments -->

View File

@@ -2,5 +2,5 @@
<?php $this->renderPartial('_form', array(
'post'=>$post,
'buttonLabel'=>'Save',
'update'=>true,
)); ?>

View File

@@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="en" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->theme->baseUrl; ?>/css/main.css" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->baseUrl; ?>/css/main.css" />
<title>Error <?php echo $data['code']; ?></title>
</head>

View File

@@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="en" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->theme->baseUrl; ?>/css/main.css" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->baseUrl; ?>/css/main.css" />
<title>Login Required</title>
</head>

View File

@@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="en" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->theme->baseUrl; ?>/css/main.css" />
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->baseUrl; ?>/css/main.css" />
<title>Page Not Found</title>
</head>

View File

@@ -0,0 +1,123 @@
Creating and Displaying Comments
================================
In this section, we implement the comment creation and display features.
Scaffolding with Comments
-------------------------
We start with scaffolding using the `yiic` tool to create the code that performs the CRUD operations about comments.
Open a command window and run the following commands:
~~~
% cd /wwwroot/blog
% protected/yiic shell
Yii Interactive Tool v1.0
Please type 'help' for help. Type 'exit' to quit.
>> crud Comment
......
>> exit
%
~~~
Like what we did with post scaffolding, the above command will generate several files that implement the CRUD operations about comments. Note that the `Comment` class was already generated when we performed scaffolding with posts.
Displaying Comments
-------------------
Instead of displaying and creating comments in dividual pages, we use the post display page. Below the post content display, we first display a list of comments belonging to that post, and we then display a comment creation form.
In order to display comments on the post page, we modify the `actionShow()` method of `PostController` as follows,
~~~
[php]
public function actionShow()
{
$post=$this->loadPost();
$this->render('show',array(
'post'=>$post,
'comments'=>$post->comments,
));
}
~~~
Note that the expression `$post->comments` would return the comments belonging to the post.
We also modify the `show` view by appending the comment display at the end of the post display.
Creating Comments
-----------------
To handle comment creation, we first modify the `actionShow()` method of `PostController` as follows,
~~~
[php]
public function actionShow()
{
$post=$this->loadPost();
$comment=$this->newComment($post);
$this->render('show',array(
'post'=>$post,
'comments'=>$post->comments,
'newComment'=>$comment,
));
}
protected function newComment($post)
{
$comment=new Comment;
if(isset($_POST['Comment']))
{
$comment->attributes=$_POST['Comment'];
$comment->postId=$post->id;
$comment->status=Comment::STATUS_PENDING;
if(isset($_POST['previewComment']))
$comment->validate('insert');
else if(isset($_POST['submitComment']) && $comment->save())
{
Yii::app()->user->setFlash('commentSubmitted','Thank you...'); $this->refresh();
}
}
return $comment;
}
~~~
In the above, we call the `newComment()` method before we render the `show` view. In the `newComment()` method, we generate a `Comment` instance and check if the comment form is submitted. The form may be submitted by clicking either the submit button or the preview button. If the former, we try to save the comment and display a flash message. The flash message is displayed only once, which means if we refresh the page again, it will disappear.
We also modify the `show` view by appending the comment creation form:
~~~
[php]
......
<?php $this->renderPartial('/comment/_form',array(
'comment'=>$newComment,
'update'=>false,
)); ?>
~~~
Here we embed the comment creation form by rendering the partial view `/wwwroot/blog/protected/views/comment/_form.php`. The variable `$newComment` is passed by the `actionShow` method. Its main purpose is to store the user comment input. The variable `update` is set as false, which indicates the comment form is being used to create a new comment.
In order to support comment preview, we add a preview button to the comment creation form. When the preview button is clicked, the comment preview is displayed at the bottom. Below is the updated code of the comment form:
~~~
[php]
...comment form with preview button...
<?php if(isset($_POST['previewComment']) && !$comment->hasErrors()): ?>
<h3>Preview</h3>
<div class="comment">
<div class="author"><?php echo $comment->authorLink; ?> says:</div>
<div class="time"><?php echo date('F j, Y \a\t h:i a',$comment->createTime); ?></div>
<div class="content"><?php echo $comment->contentDisplay; ?></div>
</div><!-- post preview -->
<?php endif; ?>
~~~
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,98 @@
Managing Comments
=================
Comment management includes updating, deleting and approving comments. These operations are implemented as actions in the `CommentController` class.
Updating and Deleting Comments
------------------------------
The code generated by `yiic` for updating and deleting comments remains largely unchanged. Because we support comment preview when updating a comment, we only need to change the `actionUpdate()` method of `CommentController` as follows,
~~~
[php]
public function actionUpdate()
{
$comment=$this->loadComment();
if(isset($_POST['Comment']))
{
$comment->attributes=$_POST['Comment'];
if(isset($_POST['previewComment']))
$comment->validate('update');
else if(isset($_POST['submitComment']) && $comment->save())
$this->redirect(array('post/show',
'id'=>$comment->postId,
'#'=>'c'.$comment->id));
}
$this->render('update',array('comment'=>$comment));
}
~~~
It is very similar to that in `PostController`.
Approving Comments
------------------
When comments are newly created, they are in pending approval status and need to be approved in order to be visible to guest users. Approving a comment is mainly about changing the status column of the comment.
We create an `actionApprove()` method in `CommentController` as follows,
~~~
[php]
public function actionApprove()
{
if(Yii::app()->request->isPostRequest)
{
$comment=$this->loadComment();
$comment->approve();
$this->redirect(array('post/show',
'id'=>$comment->postId,
'#'=>'c'.$comment->id));
}
else
throw new CHttpException(500,'Invalid request...');
}
~~~
In the above, when the `approve` action is invoked via a POST request, we call the `approve()` method defined in the `Comment` model to change the status. We then redirect the user browser to the page displaying the post that this comment belongs to.
We also modify the `actionList()` method of `Comment` to show a list of comments pending approval.
~~~
[php]
public function actionList()
{
$criteria=new CDbCriteria;
$criteria->condition='Comment.status='.Comment::STATUS_PENDING;
$pages=new CPagination(Comment::model()->count());
$pages->pageSize=self::PAGE_SIZE;
$pages->applyLimit($criteria);
$comments=Comment::model()->with('post')->findAll($criteria);
$this->render('list',array(
'comments'=>$comments,
'pages'=>$pages,
));
}
~~~
In the `list` view, we display the detail of every comment that is pending approval. In particular, we show an `approve` link button as follows,
~~~
[php]
<?php if($comment->status==Comment::STATUS_PENDING): ?>
<span class="pending">Pending approval</span> |
<?php echo CHtml::linkButton('Approve', array(
'submit'=>array('comment/approve','id'=>$comment->id),
)); ?> |
<?php endif; ?>
~~~
We use [CHtml::linkButton()] instead of [CHtml::link()] because the former would trigger a POST request while the latter a GET request. It is recommended that a GET request should not alter the data on the server. Otherwise, we face the danger that a user may inadvertently change the server-side data several times if he refreshes the page.
<div class="revision">$Id$</div>

121
docs/blog/comment.model.txt Normal file
View File

@@ -0,0 +1,121 @@
Customizing Comment Model
=========================
When we use the `yiic` tool to create CRUD operations for posts, we already created the `Model` class. In this section, we describe how to customize this class to fit for our needs.
Customizing `rules()` Method
----------------------------
We first customize the validation rules that are generated by the `yiic` tool. For comments, we need the following rules:
~~~
[php]
public function rules()
{
return array(
array('author,email,content', 'required'),
array('author,email,url','length','max'=>128),
array('email','email'),
array('url','url'),
array('verifyCode', 'captcha', 'on'=>'insert',
'allowEmpty'=>!Yii::app()->user->isGuest),
);
}
~~~
In the above, we specify that the `author`, `email` and `content` attributes are required; the length of `author`, `email` and `url` cannot exceed 128; the `email` attribute must be a valid email address; the `url` attribute must be a valid URL; and the `verifyCode` attribute should be validated as a [CAPTCHA](http://en.wikipedia.org/wiki/Captcha) code.
The `verifyCode` attribute in the above is mainly used to store the verification code that a user enters in order to leave a comment. Because it is not present in the `Comment` table, we need to explicitly declare it as a public member variable. Its validation is using a special validator named `captcha` which refers to the [CCaptchaValidator] class. Moreover, the validation will only be performed when a new comment is being inserted (see the `on` option). And for authenticated users, this is not needed (see the `allowEmpty` option).
Customizing `safeAttributes()` Method
-------------------------------------
We then customize the `safeAttributes()` method to specify which attributes can be massively assigned.
~~~
[php]
public function safeAttributes()
{
return array('author', 'email', 'url', 'content', 'verifyCode');
}
~~~
This also indicates that the comment form would consist of fields to collect the information about author, email, URL, content and verification code.
Customizing `relations()` Method
--------------------------------
When we develop the "recent comments" portlet, we need to list the most recent comments together with their corresponding post information. Therefore, we need to customize the `relations()` method to declare the relation about post.
~~~
[php]
public function relations()
{
public function relations()
{
return array(
'post'=>array(self::BELONGS_TO, 'Post', 'postId',
'joinType'=>'INNER JOIN'),
);
}
}
~~~
Notice that the join type for the `post` relation is `INNER JOIN`. This is because a comment has to belong to a post.
Customizing `attributeLabels()` Method
--------------------------------------
Finally, we need to customize the `attributeLabels()` method to declare the customized labels for the attributes. The method returns an array consisting of name-label pairs. When we call [CHtml::activeLabel()] to display an attribute label, it will first check if a customized label is declared. If not, it will use an algorithm to generate the default label.
~~~
[php]
public function attributeLabels()
{
return array(
'author'=>'Name',
'url'=>'Website',
'content'=>'Comment',
'verifyCode'=>'Verification Code',
);
}
~~~
> Tip: The algorithm for generating the default label is based on the attribute name. It first breaks the name into words according to capitalization. It then changes the first character in each word into upper case. For example, the name `verifyCode` would have the default label `Verify Code`.
Customizing Saving Process
--------------------------
Because we want to keep the comment count in each post, when we add or delete a comment, we need to adjust the corresponding comment count for the post. We achieve this by overriding the `afterSave()` method and the `afterDelete()` method. We also override the `beforeValidate()` method so that we can convert the content from the Markdown format to HTML format and record the creation time.
~~~
[php]
protected function beforeValidate($on)
{
$parser=new CMarkdownParser;
$this->contentDisplay=$parser->safeTransform($this->content);
if($this->isNewRecord)
$this->createTime=time();
return true;
}
protected function afterSave()
{
if($this->isNewRecord && $this->status==Comment::STATUS_APPROVED)
Post::model()->updateCounters(array('commentCount'=>1), "id={$this->postId}");
}
protected function afterDelete()
{
if($this->status==Comment::STATUS_APPROVED)
Post::model()->updateCounters(array('commentCount'=>-1), "id={$this->postId}");
}
~~~
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,68 @@
Final Tune-up and Deployment
============================
We are close to finish our blog application. Before deployment, we would like to do some tune-ups.
Changing Home Page
------------------
We change to use the post list page as the home page. We modify the [application configuration](http://www.yiiframework.com/doc/guide/basics.application#application-configuration) as follows,
~~~
[php]
return array(
......
'defaultController'=>'post',
......
);
~~~
> Tip: Because `PostController` already declares `list` to be its default action, when we access the home page of the application, we will see the result generated by the `list` action of the post controller.
Enabling Schema Caching
-----------------------
Because ActiveRecord relies on the metadata about tables to determine the column information, it takes time to read the metadata and analyze it. This may not be a problem during development stage, but for an application running in production mode, it is a total waste of time if the database schema does not change. Therefore, we should enable the schema caching by modifying the application configuration as follows,
~~~
[php]
return array(
......
'components'=>array(
......
'cache'=>array(
'class'=>'CDbCache',
),
'db'=>array(
'class'=>'system.db.CDbConnection',
'connectionString'=>'sqlite:/wwwroot/blog/protected/data/blog.db',
'schemaCachingDuration'=>3600,
),
),
);
~~~
In the above, we first add a `cache` component which uses a default SQLite database as the caching storage. If our server is equipped with other caching extensions, such as APC, we could change to use them as well. We also modify the `db` component by setting its [schemaCachingDuration|CDbConnection::schemaCachingDuration] property to be 3600, which means the parsed database schema data can remain valid in cache for 3600 seconds.
Disabling Debugging Mode
------------------------
We modify the entry script file `/wwwroot/blog/index.php` by removing the line defining the constant `YII_DEBUG`. This constant is useful during development stage because it allows Yii to display more debugging information when an error occurs. However, when the application is running in production mode, displaying debugging information is not a good idea because it may contain sensitive information such as where the script file is located, and the content in the file, etc.
Deploying the Application
-------------------------
The final deployment process is very simple. We follow the similar steps as we did when creating the skeleton application:
1. Install Yii in the target place if it is not available;
2. Copy the entire directory `/wwwroot/blog` to the target place;
3. Edit the entry script file `index.php` by pointing the `$yii` variable to the new Yii bootstrap file;
4. Edit the file `protected/yiic.php` by setting the `$yiic` variable to be the new Yii `yiic.php` file;
5. Change the permission of the directories `assets` and `protected/runtime` so that they are writable by the Web server process.
<div class="revision">$Id$</div>

12
docs/blog/final.error.txt Normal file
View File

@@ -0,0 +1,12 @@
Customizing Error Display
=========================
Our blog application is using the templates provided by Yii to display various errors. Because the style and wording are different from what we want, we would like to customize these templates. To do so, we create a set of view files under the directory `/wwwroot/blog/protected/views/system`.
We first create a file named `error.php`. This is the default view that will be used to display all kinds of errors if a more specific error view file is not available. Because this view file is used when an error occurs, it should not contain very complex PHP logic that may cause further errors. Note also that error view files do not use layout. Therefore, each view file should have complete page display.
We also create a file named `error401.php` to display 401 (unauthenticated) HTTP errors, and a file named `error404.php` to display 404 (page not found) HTTP errors.
To learn more details about the naming of these error view files, please refer to [the Guide](http://www.yiiframework.com/doc/guide/topics.error#displaying-errors).
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,55 @@
Future Enhancements
===================
Using a Theme
-------------
Without writing any code, our blog application is already [themeable](http://www.yiiframework.com/doc/guide/topics.theming). To use a theme, we mainly need to develop the theme by writing customized view files in the theme. For example, to use a theme named `classic` that uses a different page layout, we would create a layout view file `/wwwroot/blog/themes/classic/views/layouts/main.php`. We also need to change the application configuration to indicate our choice of the `classic` theme:
~~~
[php]
return array(
......
'theme'=>'classic',
......
);
~~~
Internationalization
--------------------
We may also internationalize our blog application so that its pages can be displayed in different languages. This mainly involves efforts in two aspects.
First, we may create view files in different languages. For example, for the `list` page of `PostController`, we can create a view file `/wwwroot/blog/protected/views/post/zh_cn/list.php`. When the application is configured to use simplified Chinese (the language code is `zh_cn`), Yii will automatically use this new view file instead of the original one.
Second, we may create message translations for those messages generated by code. The message translations should be saved as files under the directory `/wwwroot/blog/protected/messages`. We also need to modify the code where we use text strings by enclosing them in the method call `Yii::t()`.
For more details about internationalization, please refer to [the Guide](http://www.yiiframework.com/doc/guide/topics.i18n).
Improving Performance with Cache
--------------------------------
While the Yii framework itself is [very efficient](http://www.yiiframework.com/performance/), it is not necessarily true that an application written in Yii efficient. There are several places in our blog application that we can improve the performance. For example, the tag clould portlet could be one of the performance bottlenecks because it involves complex database query and PHP logic.
We can make use of the sophisticated [caching feature](http://www.yiiframework.com/doc/guide/caching.overview) provided by Yii to improve the performance. One of the most useful components in Yii is [COutputCache], which caches a fragment of page display so that the underlying code generating the fragment does not need to be executed for every request. For example, in the layout file `/wwwroot/blog/protected/views/layouts/main.php`, we can enclose the tag cloud portlet with [COutputCache]:
~~~
[php]
<?php if($this->beginCache('tagCloud', array('duration'=>3600))) { ?>
<?php $this->widget('TagCloud'); ?>
<?php $this->endCache(); } ?>
~~~
With the above code, the tag cloud display will be served from cache instead of being generated on-the-fly for every request. The cached content will remain valid in cache for 3600 seconds.
Adding New Features
-------------------
Our blog application only has very basic functionalities. To become a complete blog system, more features are needed, for example, calendar portlet, email notifications, post categorization, archived post portlet, and so on. We will leave the implementation of thse features to interested readers.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,35 @@
Logging Errors
==============
A production Web application often needs sophisticated logging for various events. In our blog application, we would like to log the errors occurring when it is being used. Such errors could be programming mistakes or users' misuage of the system. Logging these errors will help us to improve the blog application.
We enable the error logging by modifying the [application configuration](http://www.yiiframework.com/doc/guide/basics.application#application-configuration) as follows,
~~~
[php]
return array(
'preload'=>array('log'),
......
'components'=>array(
'log'=>array(
'class'=>'CLogRouter',
'routes'=>array(
array(
'class'=>'CFileLogRoute',
'levels'=>'error, warning',
),
),
),
......
),
);
~~~
With the above configuration, if an error or warning occurs, detailed information will be logged and saved in a file located under the directory `/wwwroot/blog/protected/runtime`.
The `log` component offers more advanced features, such as sending log messages to a list of email addresses, displaying log messages in JavaScript console window, etc. For more details, please refer to [the Guide](http://www.yiiframework.com/doc/guide/topics.logging).
<div class="revision">$Id$</div>

45
docs/blog/final.url.txt Normal file
View File

@@ -0,0 +1,45 @@
Beautifying URLs
================
The URLs linking various pages of our blog application currently look ugly. For example, the URL for the page showing a post looks like the following:
~~~
/index.php?r=post/show&id=1
~~~
In this section, we describe how to beautifying these URLs and make them SEO-friendly. We goal is to be able to use the following URLs in the application:
* `/index.php/tag/yii`: leads to the page showing a list of posts with tag `yii`;
* `/index.php/posts`: leads to the page showing the latest posts;
* `/index.php/post/1`: leads to the page showing the detail of the post with ID 1;
* `/index.php/post/update/1`: leads to the page that allows updating the post with ID 1.
To achieve our goal, we modify the [application configuration](http://www.yiiframework.com/doc/guide/basics.application#application-configuration) as follows,
~~~
[php]
return array(
......
'components'=>array(
......
'urlManager'=>array(
'urlFormat'=>'path',
'rules'=>array(
'tag/<tag>'=>'post/list',
'posts'=>'post/list',
'post/<id:\d+>'=>'post/show',
'post/update/<id:\d+>'=>'post/update',
),
),
),
);
~~~
In the above, we configure the [urlManager](http://www.yiiframework.com/doc/guide/topics.url) component by setting its `urlFormat` property to be `path` and adding a set of `rules`.
The rules are used by `urlManager` to parse and create the URLs in the desired format. For example, the first rule says that if a URL `/index.php/tag/yii` is requested, the `urlManager` component should be responsible to dispatch the request to the [route](http://www.yiiframework.com/doc/guide/basics.controller#route) `post/list` and generate a `tag` GET parameter with the value `yii`. On the other hand, when creating a URL with the route `post/list` and parameter `tag`, the `urlManager` component will also use this rule to generate the desired URL `/index.php/tag/yii`. For this reason, we say that `urlManager` is a two-way URL manager.
The `urlManager` component can further beautify our URLs, such as hiding `index.php` in the URLs, appending suffix like `.html` to the URLs. We can obtain these features easily by configuring various properties of `urlManager` in the application configuration. For more details, please refer to [the Guide](http://www.yiiframework.com/doc/guide/topics.url).
<div class="revision">$Id$</div>

10
docs/blog/index.txt Normal file
View File

@@ -0,0 +1,10 @@
The Yii Blog Tutorial
=====================
This tutorial describes how we use Yii to develop a blog Web application like [the Yii blog demo](http://www.yiiframework.com/demos/blog/). For complete documentation about Yii, readers should refer to [the Definitive Guide to Yii](http://www.yiiframework.com/doc/guide/) and [the Yii Class Reference](http://www.yiiframework.com/doc/api/).
Readers of this tutorial are not required to have prior knowledge about Yii. However, readers should have basic knowledge of object-oriented programming (OOP) and database programming.
This tutorial is released under [the Terms of Yii Documentation](http://www.yiiframework.com/doc/terms/).
<div class="revision">$Id$</div>

109
docs/blog/portlet.base.txt Normal file
View File

@@ -0,0 +1,109 @@
Creating Portlet Architecture
=============================
Features like "the most recent comments", "tag cloud" are better to be implemented in [portlets](http://en.wikipedia.org/wiki/Portlet). A portlet is a pluggable user interface component that renders a fragment of HTML code. In this section, we describe how to set up the portlet architecture for our blog application.
Based on the requirements analysis, we need four different portlets: the login portlet, the "user menu" portlet, the "tag cloud" portlet and the "recent comments" portlet. These portlets will be placed in the side bar section of every page.
Creating `Portlet` Class
------------------------
We define a class named `Portlet` to serve as the base class for all our portlets. The base class contains the common properties and methods shared by all portlets. For example, it defines a `title` property that represents the title of a portlet; it defines how to decorate a portlet using a framed box with colored background.
The following code shows the definition of the `Portlet` base class. Because a portlet often contains both logic and presentation, we define `Portlet` by extending [CWidget], which means a portlet is a [widget](http://www.yiiframework.com/doc/guide/basics.view) and can be embedded in a view using the [widget()|CBaseController::widget] method.
~~~
[php]
class Portlet extends CWidget
{
public $title; // the portlet title
public $visible=true; // whether the portlet is visible
// ...other properties...
public function init()
{
if($this->visible)
{
// render the portlet starting frame
// render the portlet title
}
}
public function run()
{
if($this->visible)
{
$this->renderContent();
// render the portlet ending frame
}
}
protected function renderContent()
{
// child class should override this method
// to render the actual body content
}
}
~~~
In the above code, the `init()` and `run()` methods are required by [CWidget], which are called automatically when the widget is being rendered in a view. Child classes of `Portlet` mainly need to override the `renderContent()` method to generate the actual portlet body content.
We save the `Portlet` class in the file `/wwwroot/blog/protected/components/Portlet.php`. We do this for two reasons. First, Yii promotes the convention that a class file is named as the corresponding class name with the suffix `.php`. Second, the directory `/wwwroot/blog/protected/components` has been added to the PHP `include_path` and class files under that directory can be automatically loaded by Yii when the corresponding classes are referenced for the first time. If we take a closer look at the application configuration, we shall find the following code:
~~~
[php]
return array(
......
'import'=>array(
'application.models.*',
'application.components.*',
),
......
);
~~~
The above lines instruct Yii to import the two path aliases: `application.models.*` and `application.components.*`, which is equivalent to adding the directories `/wwwroot/blog/protected/models` and `/wwwroot/blog/protected/components` to the PHP `include_path`. For more details, please refer to [the Guide](http://www.yiiframework.com/doc/guide/basics.namespace).
Customizing Page Layout
-----------------------
It is time for us to adjust the page layout so that we can place portlets in the side bar section. The page layout is solely determined by the layout view file `/wwwroot/blog/protected/views/layouts/main.php`. It renders the common sections (e.g. header, footer) of different pages and embeds at an appropriate place the dynamic content that are generated by individual action views.
Our blog application will use the following layout:
*show layout diagram*
The corresponding layout code looks like the following:
~~~
[php]
<html>
<head>
......
<?php echo CHtml::cssFile(Yii::app()->baseUrl.'/css/main.css'); ?>
<title><?php echo $this->pageTitle; ?></title>
</head>
<body>
...header...
<div id="sidebar">
...list of portlets...
</div>
<div id="content">
<?php echo $content; ?>
</div>
...footer...
</body>
</html>
~~~
Besides customizing the layout view file, we also need to adjust the CSS file `/wwwroot/blog/css/main.css` so that the overall appearance would look like what we see in the [blog demo](http://www.yiiframework.com/demos/blog/). We will not go into details here.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,80 @@
Creating Recent Comments Portlet
================================
In this section, we create the last portlet that displays a list of comments recently published.
Creating `RecentComments` Class
-------------------------------
We create the `RecentComments` class in the file `/wwwroot/blog/protected/components/RecentComments.php`. The file has the following content:
~~~
[php]
<?php
class RecentComments extends Portlet
{
public $title='Recent Comments';
public function getRecentComments()
{
return Comment::model()->findRecentComments();
}
protected function renderContent()
{
$this->render('recentComments');
}
}
~~~
In the above we invoke the `findRecentComments` method which is defined in the `Comment` class as follows,
~~~
[php]
class Comment extends CActiveRecord
{
......
public function findRecentComments($limit=10)
{
$criteria=array(
'condition'=>'Comment.status='.self::STATUS_APPROVED,
'order'=>'Comment.createTime DESC',
'limit'=>$limit,
);
return $this->with('post')->findAll($criteria);
}
}
~~~
Creating `recentComments` View
-------------------------
The `recentComments` view is saved in the file `/wwwroot/blog/protected/components/views/recentComments.php`. The view simply displays every comment returned by the `RecentComments::getRecentComments()` method.
Using `RecentComments` Portlet
------------------------------
We modify the layout file `/wwwroot/blog/protected/views/layouts/main.php` to embed this last portlet,
~~~
[php]
......
<div id="sidebar">
<?php $this->widget('UserLogin',array('visible'=>Yii::app()->user->isGuest)); ?>
<?php $this->widget('UserMenu',array('visible'=>!Yii::app()->user->isGuest)); ?>
<?php $this->widget('TagCloud'); ?>
<?php $this->widget('RecentComments'); ?>
</div>
......
~~~
<div class="revision">$Id$</div>

109
docs/blog/portlet.login.txt Normal file
View File

@@ -0,0 +1,109 @@
Creating Login Portlet
======================
The skeleton application we created already contains a login page. In this section, we will convert this page into a login portlet named `UserLogin`. The portlet will be displayed in the side bar section of pages when the current user is a guest user who is not authenticated. If he logs in successfully, the portlet will disappear and the previously developed user menu portlet will show up.
Creating `UserLogin` Class
--------------------------
Like the user menu portlet, we create the `UserLogin` class to contain the logic of the user login portlet and save it in the file `/wwwroot/blog/protected/components/UserLogin.php`. The file has the following content:
~~~
[php]
<?php
class UserLogin extends Portlet
{
public $title='Login';
protected function renderContent()
{
$form=new LoginForm;
if(isset($_POST['LoginForm']))
{
$form->attributes=$_POST['LoginForm'];
if($form->validate())
$this->controller->refresh();
}
$this->render('userLogin',array('form'=>$form));
}
}
~~~
The code in the `renderContent()`method is copied from the `actionLogin()` method of `SiteController` that we generated at the beginning using the `yiic` tool. We mainly change the `render()` method call by rendering a view named `userLogin`. Notice also that we create an object of the `LoginForm` class in this method. The class represents the user input that we collect from the login form. It is in the file `/wwwroot/blog/protected/models/LoginForm.php` and is generated by the `yiic` tool when we create the skeleton application.
Creating `userLogin` View
-------------------------
The content of the `userLogin` view also comes largely from the `login` view for the `SiteController`'s `login` action. The view is saved in the file `/wwwroot/blog/protected/components/views/loginUser.php` and has the following content:
~~~
[php]
<?php echo CHtml::form(); ?>
<div class="row">
<?php echo CHtml::activeLabel($form,'username'); ?>
<br/>
<?php echo CHtml::activeTextField($form,'username') ?>
<?php echo CHtml::error($form,'username'); ?>
</div>
<div class="row">
<?php echo CHtml::activeLabel($form,'password'); ?>
<br/>
<?php echo CHtml::activePasswordField($form,'password') ?>
<?php echo CHtml::error($form,'password'); ?>
</div>
<div class="row">
<?php echo CHtml::activeCheckBox($form,'rememberMe'); ?>
<?php echo CHtml::label('Remember me next time',CHtml::getActiveId($form,'rememberMe')); ?>
</div>
<div class="row">
<?php echo CHtml::submitButton('Login'); ?>
<p class="hint">You may login with <b>demo/demo</b></p>
</div>
</form>
~~~
In the login form, we display a username text field and a password field. We also display a check box indicating whether the user login status should be remembered even if the browser is closed. The view has a local variable named `$form` which comes from the data passed to the `render()` method call in `UserLogin::renderContent()`.
Because `LoginForm` data model contains validation rules (like in the `Post` model), when a user submits the form, the model will perform data validation. If there is any validation error, the form will display it next to the incorrect input field via [CHtml::error()].
Using `UserLogin` Portlet
-------------------------
We use `UserLogin` like we do with `UserMenu` by modifying the layout file `/wwwroot/blog/protected/views/layouts/main.php` as follows,
~~~
[php]
......
<div id="sidebar">
<?php $this->widget('UserLogin',array('visible'=>Yii::app()->user->isGuest)); ?>
<?php $this->widget('UserMenu',array('visible'=>!Yii::app()->user->isGuest)); ?>
</div>
......
~~~
Notice that `UserLogin` is visible only when the current user is a guest, which is contrary to `UserMenu`.
Testing `UserLogin` Portlet
---------------------------
To test the `UserLogin` portlet, follow the steps below:
1. Access the URL `http://www.example.com/blog/index.php`. If the current user is not logged in, we should be able to see the `UserLogin` portlet.
2. Without entering anything in the login form, if we click the `Login` button, we should see error messages.
3. Try logging in with username `demo` and password `demo`. The current page will be refreshed, the `UserLogin` portlet disappears, and the `UserMenu` portlet appears.
4. Click on the `Logout` menu item in the `UserMenu` portlet, we should see that the `UserMenu` portlet disappears while the `UserLogin` portlet appears again.
Summary
-------
The `UserLogin` portlet is a typical example that follows the MVC design pattern. It uses the `LoginForm` model to represent the data and business rules; it uses the `userLogin` view to generate user interface; and it uses the `UserLogin` class (a mini controller) to coordinate the model and the view.
<div class="revision">$Id$</div>

119
docs/blog/portlet.menu.txt Normal file
View File

@@ -0,0 +1,119 @@
Creating User Menu Portlet
==========================
In this section, we will develop our first concrete portlet - the user menu portlet which displays a list of menu items that are only available to authenticated users. The menu contains four items:
* Approve Comments: a hyperlink that leads to a list of comments pending approval;
* Create New Post: a hyperlink that leads to the post creation page;
* Manage Posts: a hyperlink that leads to the post management page;
* Logout: a link button that would log out the current user.
Creating `UserMenu` Class
-------------------------
We create the `UserMenu` class to represent the logic part of the user menu portlet. The class is saved in the file `/wwwroot/blog/protected/components/UserMenu.php` which has the following content:
~~~
[php]
<?php
class UserMenu extends Portlet
{
public function init()
{
$this->title=CHtml::encode(Yii::app()->user->name);
parent::init();
}
protected function renderContent()
{
$this->render('userMenu');
}
}
~~~
The `UserMenu` class extends from the `Portlet` class that we created previously. It overrides both the `init()` method and the `renderContent()` method of `Portlet`. The former sets the portlet title to be the name of the current user; the latter generates the portlet body content by rendering a view named `userMenu`.
> Tip: Notice that we do not explicitly include the class file for `Portlet` even though we reference it in the code. This is due to the reason we explained in the previous section.
Creating `userMenu` View
------------------------
Next, we create the `userMenu` view which is saved in the file `/wwwroot/blog/protected/components/views/userMenu.php`:
~~~
[php]
<ul>
<li><?php echo CHtml::link('Approve Comments', array('comment/list'))
. ' (' . Comment::model()->pendingCommentCount . ')'; ?></li>
<li><?php echo CHtml::link('Create New Post',array('post/create')); ?></li>
<li><?php echo CHtml::link('Manage Posts',array('post/admin')); ?></li>
<li><?php echo CHtml::linkButton('Logout',array(
'submit'=>'',
'params'=>array('command'=>'logout'),
)); ?></li>
</ul>
~~~
> Info: By default, view files for a widget should be placed under the `views` sub-directory of the directory containing the widget class file. The file name must be the same as the view name.
In the view, we call [CHtml::link] to create the needed hyperlinks; we also call [CHtml::linkButton] to create a link button which works like a normal push button. When the button is clicked, it submits an implicit form to the current page with the parameter `command` whose value is `logout`.
In order to respond to the clicking of the `logout` hyperlink, we need to modify the `init()` method of `UserMenu` as follows:
~~~
[php]
public function init()
{
if(isset($_POST['command']) && $_POST['command']==='logout')
{
Yii::app()->user->logout();
$this->controller->redirect(Yii::app()->homeUrl);
}
$this->title=CHtml::encode(Yii::app()->user->name);
parent::init();
}
~~~
In the `init()` method, we check if there is a `command` POST variable whose value is `logout`. If so, we log out the current user and redirect the user browser to the application's home page. Note that the `redirect()` method will implicitly terminate the execution of the current application.
Using `UserMenu` Portlet
------------------------
It is time for us to make use of our newly completed `UserMenu` portlet. We modify the layout view file `/wwwroot/blog/protected/views/layouts/main.php` as follows:
~~~
[php]
......
<div id="sidebar">
<?php $this->widget('UserMenu',array('visible'=>!Yii::app()->user->isGuest)); ?>
</div>
......
~~~
In the above, we call the `widget()` method to generate and execute an instance of the `UserMenu` class. Because the portlet should only be displayed to authenticated users, we toggle its `visible` property according to the `isGuest` property of the current user.
Testing `UserMenu` Portlet
--------------------------
Let's test what we have so far.
1. Open a browser window and enter the URL `http://www.example.com/blog/index.php`. Verify that there is nothing displayed in the side bar section of the page.
2. Click on the `Login` hyperlink and fill out the login form to login. If successful, verify that the `UserMenu` portlet appears in the side bar and the portlet has the username as its title.
3. Click on the 'Logout' hyperlink in the `UserMenu` portlet. Verify that the logout action is successful and the `UserMenu` portlet disappears.
Summary
-------
What we have created is a portlet that is highly reusable. We can easily reuse it in a different project with little or no modification. Moreover, the design of this portlet follows closely the philosophy that logic and presentation should be separated. While we did not point this out in the previous sections, such practice is used nearly everywhere in a typical Yii application.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,60 @@
Creating Tag Cloud Portlet
==========================
[Tag cloud](http://en.wikipedia.org/wiki/Tag_cloud) displays a list of post tags with visual decorations hinting the popularity of each individual tag.
Creating `TagCloud` Class
-------------------------
We create the `TagCloud` class in the file `/wwwroot/blog/protected/components/TagCloud.php`. The file has the following content:
~~~
[php]
<?php
class TagCloud extends Portlet
{
public $title='Tags';
public function getTagWeights()
{
return Tag::model()->findTagWeights();
}
protected function renderContent()
{
$this->render('tagCloud');
}
}
~~~
In the above we invoke the `findTagWeights` method which is defined in the `Tag` class. The method returns a list of tags with their relative frequency weights. If a tag is associated with more posts, it receives higher weights. We will use the weights to control how the tags are displayed.
Creating `tagCloud` View
-------------------------
The `tagCloud` view is saved in the file `/wwwroot/blog/protected/components/views/tagCloud.php`. For each tag returned by `TagCloud::getTagWeights()`, it displays a hyperlink which would lead to the page listing the posts with that tag. The font size of the link is determined according to the weight value of the tag. The higher the weight, the bigger the fone size.
Using `TagCloud` Portlet
-------------------------
Usage of the `TagCloud` portlet is very simple. We modify the layout file `/wwwroot/blog/protected/views/layouts/main.php` as follows,
~~~
[php]
......
<div id="sidebar">
<?php $this->widget('UserLogin',array('visible'=>Yii::app()->user->isGuest)); ?>
<?php $this->widget('UserMenu',array('visible'=>!Yii::app()->user->isGuest)); ?>
<?php $this->widget('TagCloud'); ?>
</div>
......
~~~
<div class="revision">$Id$</div>

224
docs/blog/post.crud.txt Normal file
View File

@@ -0,0 +1,224 @@
Customizing CRUD Operations
===========================
With the `Post` model ready, we need to fine-tune the actions and views for the controller `PostController`.
Customizing Access Control
--------------------------
The first thing we want to do is to customize the [access control](http://www.yiiframework.com/doc/guide/topics.auth#access-control-filter) so that we can see the `admin` page. The default access control generated by the `yiic` tool only allows a user named `admin` to access that page.
We modify the `accessRules()` method in `PostController` as follows,
~~~
[php]
public function accessRules()
{
return array(
array('allow', // allow all users to perform 'list' and 'show' actions
'actions'=>array('list', 'show'),
'users'=>array('*'),
),
array('allow', // allow authenticated users to perform any action
'users'=>array('@'),
),
array('deny', // deny all users
'users'=>array('*'),
),
);
}
~~~
The above rules state that all users can access the `list` and `show` actions, and authenticated users can access any actions, including the `admin` action. The user should be denied access in any other scenario.
Customizing `admin` Operation
-----------------------------
The only thing that we need to customize about the `admin` operation is its view file `/wwwroot/blog/protected/views/post/admin.php`. We modify this file by removing those unwanted columns and formatting the display of date values.
Customizing `show` Operation
----------------------------
We now modify the `show` action's view file `/wwwroot/blog/protected/views/post/show.php` so that it displays a post like what we see in the [blog demo](http://www.yiiframework.com/demos/blog/). The new view should display the status of a post. Instead of displaying it as the integer value, we would like to show more meaningful text display, such as `published`, `archived`. We can place this logic in the view, but a better approach is to define a `statusText` property in the `Post` model which should return the text string corresponding to the current status value.
~~~
[php]
class Post extends CActiveRecord
{
const STATUS_DRAFT=0;
const STATUS_PUBLISHED=1;
const STATUS_ARCHIVED=2;
......
public function getStatusOptions()
{
return array(
self::STATUS_DRAFT=>'Draft',
self::STATUS_PUBLISHED=>'Published',
self::STATUS_ARCHIVED=>'Archived',
);
}
public function getStatusText()
{
$options=$this->statusOptions;
return isset($options[$this->status]) ? $options[$this->status]
: "unknown ({$this->status})";
}
}
~~~
In the above, we declare class constants to represent the status values, and we define `getStatusOptions()` and `getStatusText()` methods to translate status value from integers to text strings.
Because a post can be published or unpublished, and guest users should only see published posts, we also need to modify the `loadPost()` method in the `PostController` class. In particular, if `Yii::app()->user->isGuest` is true and the requested post's status is not 1 (published), we would throw an HTTP exception.
> Tip: Yii captures HTTP exceptions (instances of [CHttpException]) and displays them in error pages using some predefined templates. These templates can be customized per application, which we will describe in detail at the end of this tutorial.
Customizing `list` Operation
----------------------------
Like the `show` operation, we customize `list` in two places: the `actionList()` method in `PostController` and the view file `/wwwroot/blog/protected/views/post/list.php`. Our customization will mainly be based on the following two requirements:
* The posts should be displayed according to their creation time with the most recent posts being displayed first.
* The system should also be able to display posts with a specified tag.
To fulfill these requirements, we modify the `actionList()` method in `PostController` as follows,
~~~
[php]
public function actionList()
{
$criteria=new CDbCriteria;
$criteria->condition='status='.Post::STATUS_PUBLISHED;
$criteria->order='createTime DESC';
$withOption=array('author');
if(!empty($_GET['tag']))
{
$withOption['tagFilter']['params'][':tag']=$_GET['tag'];
$postCount=Post::model()->with($withOption)->count($criteria);
}
else
$postCount=Post::model()->count($criteria);
$pages=new CPagination($postCount);
$pages->applyLimit($criteria);
$posts=Post::model()->with($withOption)->together(true)->findAll($criteria);
$this->render('list',array(
'posts'=>$posts,
'pages'=>$pages,
));
}
~~~
In the above, we first create a query criteria which specifies only published posts should be listed and they should be sorted according to their creation time in descending order. We then compute the total number of posts satisfying the criteria. The number is used by the pagination component to correctly compute how many pages the posts should be displayed in. Finally, we retrieve the post data from the database and send them to the `list` view for display.
Notice that when there is `tag` GET parameter, we would query with the `tagFilter` using the corresponding GET parameter value. We also call `together(true)` to ensure that only a single SQL JOIN statement is used to retrieve the posts with the specified tag. Without this call, Yii would break the query into two separate SQL statements (for efficiency concern) and would return incorrect results.
Customizing `create` and `update` Operations
--------------------------------------------
The `create` and `update` operations are very similar. They both need to display an HTML form to collect user inputs, validate them, and save them into database. The main difference is that the `update` operation will pre-populate the form with the existing post data found in the database. For this reason, the `yiic` tool generates a partial view `/wwwroot/blog/protected/views/post/_form.php` that is embedded in both the `create` and `update` views to render the needed HTML form.
We first change the `_form.php` file so that the HTML form only collects the inputs we want: `title`, `content` and `status`. We use plain text fields to collect inputs for the first two attributes, and a dropdown list to collect input for `status`. The dropdown list options are the text displays of possible post statuses:
~~~
[php]
<?php echo CHtml::activeDropDownList($post,'status',Post::model()->statusOptions); ?>
~~~
We then modify the `Post` class so that it can automatically set some attributes (e.g. `createTime`, `authorId`) before a post is saved to the database. We override the `beforeValidate()` method as follows,
~~~
[php]
protected function beforeValidate($on)
{
$parser=new CMarkdownParser;
$this->contentDisplay=$parser->safeTransform($this->content);
if($this->isNewRecord)
{
$this->createTime=$this->updateTime=time();
$this->authorId=Yii::app()->user->id;
}
else
$this->updateTime=time();
return true;
}
~~~
The method will be invoked automatically when we call `validate()` or `save()` method of the model. In this method, we use [CMarkdownParser] to convert the content from [Markdown format](http://daringfireball.net/projects/markdown/) into HTML and save the result to `contentDisplay`. This avoids repeated format conversion when we display a post. If the post is new, we set its `createTime` and `authorId` attributes; otherwise we set its `updateTime` to be the current time.
Because we want to save post tags to the `Tag` table, we also need the following method in the `Post` class, which is invoked automatically after a post is saved to the database:
~~~
[php]
protected function afterSave()
{
if(!$this->isNewRecord)
$this->dbConnection->createCommand(
'DELETE FROM PostTag WHERE postId='.$this->id)->execute();
foreach($this->getTagArray() as $name)
{
if(($tag=Tag::model()->findByAttributes(array('name'=>$name)))===null)
{
$tag=new Tag(array('name'=>$name));
$tag->save();
}
$this->dbConnection->createCommand(
"INSERT INTO PostTag (postId, tagId) VALUES ({$this->id},{$tag->id})")->execute();
}
}
~~~
In the above, we first clean up the `PostTag` table for rows related with the current post. We then insert new tags into the `Tag` table and add a reference in the `PostTag` table.
> Tip: It is good practice to keep business logic, such as the above `beforeValidate()` and `afterSave()` code, in model classes.
Implementing Preview Feature
----------------------------
Besides the above customizations, we also want to add the preview feature that would allow us to preview a post before we save it to the database.
We first change the `_form.php` view file to add a `preview` button and a preview display. The preview is only displayed when the preview button is clicked and there is not validation error.
~~~
[php]
<?php echo CHtml::submitButton('Preview',array('name'=>'previewPost')); ?>
......
<?php if(isset($_POST['previewPost']) && !$post->hasErrors()): ?>
...display preview of $post here...
<?php endif; ?>
~~~
We then change the `actionCreate()` and `actionUpdate()` methods of `PostController` to respond to the preview request. Below we show the updated code of `actionCreate()`, which is very similar to that in `actionUpdate()`:
~~~
[php]
public function actionCreate()
{
$post=new Post;
if(isset($_POST['Post']))
{
$post->attributes=$_POST['Post'];
if(isset($_POST['previewPost']))
$post->validate();
else if(isset($_POST['submitPost']) && $post->save())
$this->redirect(array('show','id'=>$post->id));
}
$this->render('create',array('post'=>$post));
}
~~~
In the above, if the preview button is clicked, we call `$post->validate()` to validate the user input; otherwise if the submit button is clicked, we try to save the post by calling `$post->save()` which implicitly performs validation. If the saving is successful (no validation errors and the data is saved to the database without error), we redirect the user browser to show the newly created post.
<div class="revision">$Id$</div>

117
docs/blog/post.model.txt Normal file
View File

@@ -0,0 +1,117 @@
Customizing Post Model
======================
The `Post` model class generated by the `yiic` tool mainly needs to be modified in three places:
- the `rules()` method: specifies the validation rules for the model attributes;
- the `relations()` method: specifies the related objects;
- the `safeAttributes()` method: specifies which attributes can be massived assigned (mainly used when passing user input to the model);
> Info: A [model](http://www.yiiframework.com/doc/guide/basics.model) consists of a list of attributes, each associated with a column in the corresponding database table. Attributes can be declared explicitly as class member variables or implicitly without any declaration.
Customizing `rules()` Method
----------------------------
We first customize the validation rules that are generated by the `yiic` tool. The validation rules ensure that user inputs fulfill certain requirements before they are saved to the database. For example, the `status` attribute should be an integer 0, 1 or 2. The default rules generated by the `yiic` tool are based on the table column information and may not satisfy the actual requirements.
Based on the requirements analysis, we modify the `rules()` method as follows:
~~~
[php]
public function rules()
{
return array(
array('title, content, status', 'required'),
array('title', 'length', 'max'=>128),
array('status', 'in', 'range'=>array(0, 1, 2)),
array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/',
'message'=>'Tags can only contain word characters.'),
);
}
~~~
In the above, we specify that the `title`, `length` and `max` attributes are required; the length of `title` should not exceed 128; the `status` attribute value should be 0 (draft), 1 (published) or 2 (archived); and the `tags` attribute should only contain word characters and commas. All other attributes (e.g. `id`, `createTime`) will not be validated because their values do not come from user input.
After making these changes, we can visit the post creation page again to verify the new validation rules are taking effect.
> Info: Validation rules are used when we call the [validate()|CModel::validate] or [save()|CActiveRecord::save] method of the model instance.
Customizing `safeAttributes()` Method
-------------------------------------
We then customize the `safeAttributes()` method to specify which attributes can be massively assigned. When passing user inputs to the model instance, we often use the following massive assignment to simplify our code:
~~~
[php]
$model->attributes=$_POST['Post'];
~~~
Without using the above massive assignment, we would end up with the following lengthy code:
~~~
[php]
$model->title=$_POST['Post']['title'];
$model->content=$_POST['Post']['content'];
......
~~~
Although massive assignment is very convenient, it has a potential danger that a malicious user may attempt to populate an attribute whose value should remain read only or should only be changed by developer in code. For example, the `id` of the post currently being updated should not be changed.
To prevent from such danger, we would customize the `safeAttributes()` as follows, which states only `title`, `content`, `status` and `tags` attributes can be massively assigned:
~~~
[php]
public function safeAttributes()
{
return array('title', 'content', 'status', 'tags');
}
~~~
> Tip: An easy way to identity which attributes should be put in the safe list is by observing the HTML form that is used to collect user input. Model attributes that appear in the form to receive user input may be declared as safe. Since these attributes receive input from end users, they usually should be associated with some validation rules.
Customizing `relations()` Method
--------------------------------
Lastly we customize the `relations()` method to specify the related objects of a post. By declaring these related objects in `relations()`, we can exploit the powerful [Relational ActiveRecord (RAR)](http://www.yiiframework.com/doc/guide/database.arr) feature to access the relation information of a post, such as its author and comments, without writing complex SQL JOIN statements.
We customize the `relations()` method as follows:
~~~
[php]
public function relations()
{
return array(
'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
'comments'=>array(self::HAS_MANY, 'Comment', 'postId',
'order'=>'??.createTime'),
'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
'joinType'=>'INNER JOIN', 'condition'=>'??.name=:tag'),
);
}
~~~
The above relations state that
* A post belongs to an author whose class is `User` and the relationship is established based on the `authorId` attribute value of the post;
* A post has many comments whose class is `Comment` and the relationship is established based on the `postId` attribute value of the comments. These comments should be sorted according to their creation time.
* We will explain the `tagFilter` relation in the next section.
For more details about how to declare relations, please refer to [the Guide](http://www.yiiframework.com/doc/guide/database.arr).
With the above relation declaration, we can easily access the author and comments of a post like the following:
~~~
[php]
$author=$post->author;
echo $author->username;
$comments=$post->comments;
foreach($comments as $comment)
echo $comment->content;
~~~
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,58 @@
Scaffolding with Posts
======================
Post management is one of the main requirements for the blog application. It includes creating, reading, updating and deleting (CRUD) posts. In this section, we will use the `yiic` tool to create rapidly a prototype about post management. This process is also known as *scaffolding*.
Open a command window and run the following commands:
~~~
% cd /wwwroot/blog
% protected/yiic shell
Yii Interactive Tool v1.0
Please type 'help' for help. Type 'exit' to quit.
>> model Post
......
>> model Tag
......
>> model Comment
......
>> crud Post
......
>> exit
%
~~~
The files generated by the above commands are located under `/wwwroot/blog/protected` and can be classified into [model](http://www.yiiframework.com/doc/guide/basics.model), [controller](http://www.yiiframework.com/doc/guide/basics.controller) and [view](http://www.yiiframework.com/doc/guide/basics.view) files:
- model files:
* `models/Post.php` contains the `Post` class that extends from [CActiveRecord] and can be used to access the `Post` database table;
* `models/Tag.php` contains the `Tag` class that extends from [CActiveRecord] and can be used to access the `Tag` database table;
* `models/Comment.php` contains the `Comment` class that extends from [CActiveRecord] and can be used to access the `Comment` database table;
- controller file:
* `controllers/PostController.php` contains the `PostController` class which is the controller in charge of all CRUD operations about posts;
- view files:
* `views/post/create.php` is the view file that shows an HTML form to create a new post;
* `views/post/update.php` is the view file that shows an HTML form to update an existing post;
* `views/post/show.php` is the view file that displays the detailed information of a post;
* `views/post/list.php` is the view file that displays a list of posts;
* `views/post/admin.php` is the view file that displays posts in a table with administrative commands.
* `views/post/_form.php` is the partial view file that displays the HTML form for collecting post information. It is embedded in the `create` and `update` views.
When a user requests to display the latest posts, the following steps occurs to the blog application:
1. The [entry script](http://www.yiiframework.com/doc/guide/basics.entry) is executed by the Web server which creates and initializes an [application](http://www.yiiframework.com/doc/guide/basics.application) instance to handle the request;
2. The application creates an instance of `PostController` and executes it;
3. The `PostController` instance executes the requested `list` action by calling its `actionList()` method;
4. The `actionList()` method queries database to bring back the list of recent posts;
5. The `actionList()` method renders the `list` view with the post data.
To try out the post management feature that we just prototyped, access the URL `http://www.example.com/blog/index.php?r=post`. The feature seems to be fairly complete as it contains all the needed operations as described in the requirements.
Note that we cannot access the `admin` page in the prototype because the page is configured to be accessible only by a user named `admin`. In the next section, we will describe how to remove this constraint. We will also fine tune the generated pages to fulfill the requirements precisely.
<div class="revision">$Id$</div>

109
docs/blog/start.auth.txt Normal file
View File

@@ -0,0 +1,109 @@
Authenticating User
===================
Our blog application needs to differentiate between the system owner and guest users. Therefore, we need to implement the [user authentication](http://www.yiiframework.com/doc/guide/topics.auth) feature.
As you may have found that the skeleton application already provides such a feature since it allows us to login and logout. In this section, we will modify the code in the skeleton application so that the authentication is done against the `User` database table.
Creating `User` Class
---------------------
Because the authentication needs to read data from the `User` table, we first create [ActiveRecord](http://www.yiiframework.com/doc/guide/database.ar) class named `User` to facilitate the data accessing. ActiveRecord allows us to access the data in a table in an OOP fashion and it can accomplish most database-related tasks without requiring us to write any SQL statements.
> Info: Remember that we set up a `db` application component to connect to the SQLite database in the previous section. By default, ActiveRecord will use this `db` component as the underlying database connection.
To create the `User` class, we exploit a command line tool named `yiic` that is bundled with Yii. Open a command window and run the following commands:
~~~
% cd /wwwroot/blog
% protected/yiic shell
Yii Interactive Tool v1.0
Please type 'help' for help. Type 'exit' to quit.
>> model User
......
>> exit
%
~~~
> Info: Some PHP installations may use a different `php.ini` file for CLI PHP. As a result, when running the above `yiic` commands, you may encounter errors like "YiiBase::include(PDO.php): failed to open stream..." or "...could not find driver". Please double check your CLI PHP configuration. You may also use the following command to make sure your CLI PHP is using the specified `php.ini` file:
>
> ~~~
> php -c path/to/php.ini protected/yiic.php shell
> ~~~
The above commands will generate a `User` class in the file `/wwwroot/blog/protected/models/User.php` which has the following content:
~~~
[php]
<?php
class User extends CActiveRecord
{
public static function model($className=__CLASS__)
{
return parent::model($className);
}
public function tableName()
{
return 'User';
}
}
~~~
In the `User` class, the `model()` method is mandatory for every ActiveRecord class, and the `tableName()` method declares the name of the database table that this class is associated with. We can skip the `tableName()` method if the table name is the same as the class name.
For more details about what we can do with ActiveRecord, please check [the Guide](http://www.yiiframework.com/doc/guide/database.ar).
Creating `UserIdentity` Class
--------------------------------
User authentication is performed in a class implementing the [IUserIdentity] interface. The skeleton application already provides such a class named `UserIdentity` that is located at `/wwwroot/blog/protected/components/UserIdentity.php`. We modify this class by exploiting the `User` class we just created.
~~~
[php]
<?php
class UserIdentity extends CUserIdentity
{
private $_id;
public function authenticate()
{
$username=strtolower($this->username);
$user=User::model()->find('LOWER(username)=?',array($username));
if($user===null)
$this->errorCode=self::ERROR_USERNAME_INVALID;
else if(md5($this->password)!==$user->password)
$this->errorCode=self::ERROR_PASSWORD_INVALID;
else
{
$this->_id=$user->id;
$this->username=$user->username;
$this->errorCode=self::ERROR_NONE;
}
return !$this->errorCode;
}
public function getId()
{
return $this->_id;
}
}
~~~
In the `authenticate()` method, we use `User` class to look for a row in the `User` table whose `username` column is the same as the username that needs to be authenticated. The comparison is case-insensitive. If such a row is found, we further check the password. Only when both match, we conclude that the authentication is successful. We also override the `getId()` method so that it returns the `id` column of the `User` row. The parent implementation would return the username, instead.
In the login page implementation, we may find that a `UserIdentity` instance is passed to `Yii::app()->user->login()` if the authentication succeeds. This would store the `id` and `username` values in the `user` application component and make them globally available through `Yii::app()->user`.
> Info: People often get confused about identity and the `user` application component. The former represents a way of performing authentication, while the latter is used to represent the information related with the current user. An application can only have one `user` component, but it can have one or several identity classes, depending on what kind of authentication it supports. Once authenticated, an identity instance may pass its state information to the `user` component so that they are globally accessible via `user`.
Testing User Authentication
---------------------------
To test the modified `UserIdentity` class, we can browse the URL `http://www.example.com/blog/index.php` and try logging in with the username and password that we store in the `User` table. If we use the database provided by the [blog demo](http://www.yiiframework.com/demos/blog/], we should be able to login with username `demo` and password `demo`. Note that this blog system does not provide the user management feature. As a result, a user cannot change his account or create a new one through the Web interface. The user management feature may be considered as a future enhancement to the blog application.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,84 @@
Setting Up Database
===================
Having created a skeleton application, in this section we will set up the database needed by the blog application to store the post and comment data.
Designing Database
------------------
Based on the analysis of the requirements, the following database tables are needed for the blog system:
###`Post`
`Post` represents a post in the blog system. It mainly consists of the following data fields:
* `title`: required, title of the post;
* `content`: required, body content of the post which uses the [Markdown format](http://daringfireball.net/projects/markdown/syntax);
* `tags`: optional, a list of comma-separated words categorizing the post.
A post can be in one of the three statuses:
* `draft`: the post is in draft and is not visible to public;
* `published`: the post is published to public;
* `archived`: the post is outdated and is not visible to public.
### `Comment`
A comment is associated with a post and mainly consists of the following data fields:
* `name`: required, the author name;
* `email`: required, the author email;
* `website`: optional, the author website URL;
* `content`: required, the comment content which uses the [Markdown format](http://daringfireball.net/projects/markdown/syntax).
A comment can be in one of the two statuses:
* `pending approval`: the comment is awaiting the owner's approval and is not visible to public;
* `approved`: the comment is visible to public.
The following entity-relation (ER) diagram shows the table structure and relationships about the above tables. Note that we create a `Tag` table to store tag information so that we can implement the tag cloud feature. The `Tag` table is related to the `Post` table via `PostTag`.
*show ER diagram* here
Creating Database
-----------------
We choose to create a SQLite database with these tables. Because Yii database support is built on top of [PDO](http://www.php.net/manual/en/book.pdo.php), we can easily switch to use a different type of DBMS (e.g. MySQL, PostgreSQL) without the need to change our application code.
We create the SQLite database file `blog.db` under the directory `/wwwroot/blog/protected/data`. As required by SQLite, both the directory and the database file have to be writable by the Web server process.
> Tip: The Yii blog demo already includes a schema file that contains the SQL statements needed to create the database. The file is located at `demos/blog/protected/data/schema.sqlite.sql`. To create a SQLite database, we can use the `sqlite3` command line tool that is available from [the SQLite website](http://www.sqlite.org/download.html).
Establishing Database Connection
--------------------------------
To use the blog database, we need to configure the skeleton application by modifying its [application configuration](http://www.yiiframework.com/doc/guide/basics.application#application-configuration) which is stored as a PHP script named `/wwwroot/blog/protected/config/main.php`. The configuration is represented as an associative PHP array consisting of name-value pairs. Each name-value pair is used to initialize a property of the [application instance](http://www.yiiframework.com/doc/guide/basics.application).
We configure the `components` property of the application by adding a new entry named `db` shown as follows,
~~~
[php]
return array(
......
'components'=>array(
......
'db'=>array(
'class'=>'CDbConnection',
'connectionString'=>'sqlite:/wwwroot/blog/protected/data/blog.db',
),
),
......
);
~~~
The above configuration says that we have a `db` [application component](http://www.yiiframework.com/doc/guide/basics.application#application-component) whose class is [CDbConnection] and whose `connectionString` property should be initialized as `sqlite:/wwwroot/blog/protected/data/blog.db`.
With this configuration, we can access the DB connection object using `Yii::app()->db` at any place in our code. Note that `Yii::app()` returns the application instance that we create in the entry script. If you are interested in possible methods and properties that the DB connection has, you may refer to its [API documentation|CDbConnection]. However, in most cases we are not going to use this DB connection directly. Instead, we will use the so-called [ActiveRecord](http://www.yiiframework.com/doc/guide/database.ar) to access the database.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,27 @@
Requirements
============
The blog system that we are going to develop is a single user system. The owner of the system will be able to perform the following actions:
* Login and logout
* Create, update and delete posts
* Publish, unpublish and archive posts
* Approve and delete comments
All other users are guest users who can perform the following actions:
* Read posts
* Create comments
Additional Requirements for this system include:
* The homepage of the system should display a list of the most recent posts.
* If a page contains more than 10 posts, they should be displayed in pages.
* The system should display a post together with its comments.
* The system should be able to list posts with a specified tag.
* The system should show a cloud of tags indicating their use frequencies.
* The system should show a list of most recent comments.
* The system should be themeable.
* The system should use SEO-friendly URLs.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,78 @@
Creating Skeleton Application
=============================
To begin with, we create a skeleton application that will serve as a good starting point for our blog application.
The blog application will be created under the folder `/wwwroot/blog`, where `/wwwroot` stands for the document root of our Web server. As a result, our blog application can be accessed via URL `http://www.example.com/blog/` (you should replace `www.example.com` with the actual host name of your server).
Installing Yii
--------------
We first need to install the Yii framework. Grab a copy of the Yii release file (version 1.0.3 or above) from [www.yiiframework.com](http://www.yiiframework.com/download) and unpack it to the directory `/wwwroot/yii`. After the installation, double check to make sure that there is a directory `/wwwroot/yii/framework`.
> Tip: The Yii framework can be installed anywhere in the file system. Its `framework` directory contains all framework code and is the only directory needed when deploying an Yii application. A single installation of Yii can be used by multiple Yii applications.
Because our blog application needs to access SQLite database via [PDO](http://www.php.net/manual/en/book.pdo.php), it is required that we enable both the `pdo` and `pdo_sqlite` PHP extensions for our PHP installation. We may run the requirement checker to verify we have these extensions enabled. The requirement checker is a script that can be accessed via URL `http://www.example.com/yii/requirements/index.php`.
Creating Application
--------------------
We perform the following steps to create a skeleton application:
1. Copy the `testdrive` directory from the Yii installation to `/wwwroot/blog`;
2. Edit the file `/wwwroot/blog/index.php` by setting the `$yii` variable to be `/wwwroot/yii/framework/yii.php`;
3. Edit the file `/wwwroot/blog/protected/yiic.php` by setting the `$yiic` variable to be `/wwwroot/yii/framework/yiic.php`.
4. Change the permission of the directories `/wwwroot/blog/assets` and `/wwwroot/blog/protected/runtime` to make them writable by the Web server process.
5. If on Linux or Unix, change the permission of `/wwwroot/blog/protected/yiic` to be executable.
That's it. To try out the application we just created, open a Web browser and navigate to the URL `http://www.example.com/blog/index.php`. We shall see that our application has three fully functional pages: the homepage, the contact page and the login page.
In the following, we briefly describe what we have in this skeleton application.
###Entry Script
We have an [entry script](http://www.yiiframework.com/doc/guide/basics.entry) file `/wwwroot/blog/index.php` which has the following content:
~~~
[php]
<?php
$yii='/wwwroot/framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
// remove the following line when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
require_once($yii);
Yii::createWebApplication($config)->run();
~~~
This is the only script that Web users can directly access. The script first includes the Yii bootstrap file `yii.php`. It then creates an [application](http://www.yiiframework.com/doc/guide/basics.application) instance with the specified configuration and executes the application.
###Base Application Directory
We also have an [application base directory](http://www.yiiframework.com/doc/guide/basics.application#application-base-directory) `/wwwroot/blog/protected`. The majority of our code and data will be placed under this directory, and it should be protected from being accessed by Web users. For [Apache httpd Web server](http://httpd.apache.org/), we place under this directory a `.htaccess` file with the following content:
~~~
deny from all
~~~
For other Web servers, please refer to the corresponding manual on how to protect a directory from being accessed by Web users.
Application Workflow
--------------------
To help understand how Yii works, we describe the main workflow in our skeleton application when a user is accessing its contact page:
1. The [entry script](http://www.yiiframework.com/doc/guide/basics.entry) is executed by the Web server to process the request;
2. An [application](http://www.yiiframework.com/doc/guide/basics.application) instance is created and configured with initial property values specified in the application configuration file `/wwwroot/blog/protected/config/main.php`;
3. The application resolves the request into a [controller](http://www.yiiframework.com/doc/guide/basics.controller) and a [controller action](http://www.yiiframework.com/doc/guide/basics.controller#action). For the contact page request, it is resolved as the `site` controller and the `contact` action;
4. The application creates the `site` controller in terms of a `SiteController` instance and then executes it;
5. The `SiteController` instance executes the `contact` action by calling its `actionContact()` method;
6. The `actionContact` method renders a [view](http://www.yiiframework.com/doc/guide/basics.view) named `contact` to the Web user. Internally, this is achieved by including the view file `/wwwroot/blog/protected/views/site/contact.php` and embedding the result into the [layout](http://www.yiiframework.com/doc/guide/basics.view#layout) file `/wwwroot/blog/protected/views/layouts/main.php`.
<div class="revision">$Id$</div>

View File

@@ -0,0 +1,15 @@
Summary
=======
Let's take a break and see what we have done so far:
1. We identified the requirements to be fulfilled;
2. We installed the Yii framework;
3. We created a skeleton application;
4. We designed and created the blog database;
5. We modified the application configuration by adding the database connection;
6. We modified the authentication method to check against the `User` table.
For a new project, most of the time will be spent in step 1 and 4 for this first milestone.
<div class="revision">$Id$</div>

31
docs/blog/toc.txt Normal file
View File

@@ -0,0 +1,31 @@
* Getting Started
- [Overview](index)
- [Requirements Analysis](start.requirements)
- [Creating Skeleton Application](start.skeleton)
- [Setting Up Database](start.database)
- [Authenticating User](start.auth)
- [Summary](start.summary)
* Implementing Post Feature
- [Scaffolding with Posts](post.scaffold)
- [Customizing Post Model](post.model)
- [Customizing CRUD Operations](post.crud)
* Implementing Comment Feature
- [Customizing Comment Model](comment.model)
- [Creating and Displaying Comments](comment.create)
- [Managing Comments](comment.manage)
* Implementing Porlets
- [Creating Portlet Architecture](portlet.base)
- [Creating User Menu Portlet](portlet.menu)
- [Creating Login Portlet](portlet.login)
- [Creating Tag Cloud Portlet](portlet.tags)
- [Creating Recent Comments Portlet](portlet.comments)
* Final Work
- [Beautifying URLs](final.url)
- [Logging Errors](final.logging)
- [Customizing Error Display](final.error)
- [Final Tune-up and Deployment](final.deployment)
- [Future Enhancements](final.future)

View File

@@ -257,6 +257,10 @@ disambiguated using `aliasToken` if they appear in an expression (e.g.
- `condition`: the `WHERE` clause. It defaults to empty. Note, column
references need to be disambiguated using `aliasToken` (e.g. `??.id=10`).
- `params`: the parameters to be bound to the generated SQL statement.
This should be given as an array of name-value pairs. This option has been
available since version 1.0.3.
- `on`: the `ON` clause. The condition specified here will be appended
to the joining condition using the `AND` operator. This option has been
available since version 1.0.2.

View File

@@ -11,11 +11,11 @@ located under the directory `YiiRoot/testdrive`. We will use this as the
starting point for our first Yii application.
1. Copy the directory `YiiRoot/testdrive` to `WebRoot`;
2. Edit the file `WebRoot/testdrive/index.php` to change the path of
the Yii bootstrap file `yii.php`;
3. Edit the file `WebRoot/testdrive/protected/yiic.php` to change the path
of the file `yiic.php`.
4. Change the permission of `WebRoot/testdrive/assets` and `WebRoot/testdrive/protected/runtime` to make them writable by the Web server process.
2. Edit the file `WebRoot/testdrive/index.php` by setting the `$yii` variable to be `YiiRoot/framework/index.php`;
3. Edit the file `WebRoot/testdrive/protected/yiic.php` by setting the `$yiic` variable to be `YiiRoot/framework/yiic.php`;
4. Change the permission of `WebRoot/testdrive/assets` and `WebRoot/testdrive/protected/runtime` to make them writable by the Web server process;
5. If on Linux or Unix, change the permission of `WebRoot/testdrive/protected/yiic` to be executable.
> Note: Prior to version 1.0.3, we would use the `yiic` tool to generate
> the skeleton application. The following command should be executed

View File

@@ -43,16 +43,17 @@ session.
In the following example, we validate the given username and password
against the user table in a database using [Active
Record](/doc/guide/database.ar). We also define the `title` property to be
persistent using [CBaseUserIdentity::getState] and
[CBaseUserIdentity::setState] methods. As a result, during the whole user
session, we can obtain the user title information via
`Yii::app()->user->getState('title')`.
Record](/doc/guide/database.ar). We also override the `getId` method to
return the `_id` variable which is set during authentication (the default
implementation would return the username as the ID). During authentication,
we store the retrieved `title` information in a state with the same name by
calling [CBaseUserIdentity::setState].
~~~
[php]
class UserIdentity extends CUserIdentity
{
private $_id;
public function authenticate()
{
$record=User::model()->findByAttributes(array('username'=>$this->username));
@@ -62,24 +63,27 @@ class UserIdentity extends CUserIdentity
$this->errorCode=self::ERROR_PASSWORD_INVALID;
else
{
$this->title=$record->title;
$this->_id=$record->id;
$this->setState('title', $record->title);
$this->errorCode=self::ERROR_NONE;
}
return !$this->errorCode;
}
public function getTitle()
public function getId()
{
return $this->getState('title');
}
public function setTitle($value)
{
$this->setState('title',$value);
return $this->_id;
}
}
~~~
Information stored in a state (by calling [CBaseUserIdentity::setState]) will be passed
to [CWebUser] which stores them in a persistent storage, such as session.
These information can be accessed like properties of [CWebUser]. For example, we
can obtain the `title` information of the current user by `Yii::app()->user->title`
(This has been available since version 1.0.3. In prior versions, we should use
`Yii::app()->user->getState('title')`, instead.)
> Info: By default, [CWebUser] uses session as persistent storage for user
identity information. If cookie-based login is enabled (by setting
[CWebUser::allowAutoLogin] to be true), the user identity information may
@@ -239,6 +243,10 @@ matches.
- [verbs|CAccessRule::verbs]: specifies which request types (e.g.
`GET`, `POST`) this rule matches.
- [expression|CAccessRule::expression]: specifies a PHP expression whose value
indicates whether this rule matches. In the expression, you can use variable `$user`
which refers to `Yii::app()->user`. This option has been available since version 1.0.3.
### Handling Authorization Result

View File

@@ -98,7 +98,7 @@ EOD;
),
);
foreach(array('create','update','list','show','admin') as $action)
foreach(array('create','update','list','show','admin','_form') as $action)
{
$list[$action.'.php']=array(
'source'=>$templatePath.'/'.$action.'.php',

View File

@@ -0,0 +1,23 @@
<div class="yiiForm">
<p>
Fields with <span class="required">*</span> are required.
</p>
<?php echo "<?php echo CHtml::form(); ?>\n"; ?>
<?php echo "<?php echo CHtml::errorSummary(\${$modelVar}); ?>\n"; ?>
<?php foreach($columns as $name=>$column): ?>
<div class="simple">
<?php echo "<?php echo CHtml::activeLabelEx(\${$modelVar},'$name'); ?>\n"; ?>
<?php echo "<?php echo ".$this->generateInputField($model,$modelVar,$column)."; ?>\n"; ?>
</div>
<?php endforeach; ?>
<div class="action">
<?php echo "<?php echo CHtml::submitButton(\$update ? 'Save' : 'Create'); ?>\n"; ?>
</div>
</form>
</div><!-- yiiForm -->

View File

@@ -5,26 +5,7 @@
[<?php echo "<?php echo CHtml::link('Manage {$modelClass}',array('admin')); ?>"; ?>]
</div>
<div class="yiiForm">
<p>
Fields with <span class="required">*</span> are required.
</p>
<?php echo "<?php echo CHtml::form(); ?>\n"; ?>
<?php echo "<?php echo CHtml::errorSummary(\${$modelVar}); ?>\n"; ?>
<?php foreach($columns as $name=>$column): ?>
<div class="simple">
<?php echo "<?php echo CHtml::activeLabelEx(\${$modelVar},'$name'); ?>\n"; ?>
<?php echo "<?php echo ".$this->generateInputField($model,$modelVar,$column)."; ?>\n"; ?>
</div>
<?php endforeach; ?>
<div class="action">
<?php echo "<?php echo CHtml::submitButton('Create'); ?>\n"; ?>
</div>
</form>
</div><!-- yiiForm -->
<?php echo "<?php echo \$this->renderPartial('_form', array(
'$modelVar'=>\$$modelVar,
'update'=>false,
)); ?>"; ?>

View File

@@ -6,26 +6,7 @@
[<?php echo "<?php echo CHtml::link('Manage {$modelClass}',array('admin')); ?>"; ?>]
</div>
<div class="yiiForm">
<p>
Fields with <span class="required">*</span> are required.
</p>
<?php echo "<?php echo CHtml::form(); ?>\n"; ?>
<?php echo "<?php echo CHtml::errorSummary(\${$modelVar}); ?>\n"; ?>
<?php foreach($columns as $name=>$column): ?>
<div class="simple">
<?php echo "<?php echo CHtml::activeLabelEx(\${$modelVar},'$name'); ?>\n"; ?>
<?php echo "<?php echo ".$this->generateInputField($model,$modelVar,$column)."; ?>\n"; ?>
</div>
<?php endforeach; ?>
<div class="action">
<?php echo "<?php echo CHtml::submitButton('Save'); ?>\n"; ?>
</div>
</form>
</div><!-- yiiForm -->
<?php echo "<?php echo \$this->renderPartial('_form', array(
'$modelVar'=>\$$modelVar,
'update'=>true,
)); ?>"; ?>

View File

@@ -56,17 +56,22 @@ class CActiveFinder extends CComponent
* By default, several join statements may be generated in order to avoid
* fetching duplicated data. By calling this method, all tables will be joined
* together all at once.
* @param boolean whether we should enforce join even when a limit option is placed on the primary table query.
* Defaults to false, meaning we would still use two queries when there is a HAS_MANY/MANY_MANY relation and
* the primary table has a LIMIT option. This parameter is available since version 1.0.3.
* @return CActiveFinder the finder object
* @since 1.0.2
*/
public function together()
public function together($ignoreLimit=false)
{
$this->joinAll=true;
if($ignoreLimit)
$this->baseLimited=false;
return $this;
}
/**
* This is relational version of {@link CActiveRecord::find()}.
* This is the relational version of {@link CActiveRecord::find()}.
*/
public function find($condition='',$params=array())
{
@@ -79,7 +84,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findAll()}.
* This is the relational version of {@link CActiveRecord::findAll()}.
*/
public function findAll($condition='',$params=array())
{
@@ -89,7 +94,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findByPk()}.
* This is the relational version of {@link CActiveRecord::findByPk()}.
*/
public function findByPk($pk,$condition='',$params=array())
{
@@ -102,7 +107,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findAllByPk()}.
* This is the relational version of {@link CActiveRecord::findAllByPk()}.
*/
public function findAllByPk($pk,$condition='',$params=array())
{
@@ -112,7 +117,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findByAttributes()}.
* This is the relational version of {@link CActiveRecord::findByAttributes()}.
*/
public function findByAttributes($attributes,$condition='',$params=array())
{
@@ -125,7 +130,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findAllByAttributes()}.
* This is the relational version of {@link CActiveRecord::findAllByAttributes()}.
*/
public function findAllByAttributes($attributes,$condition='',$params=array())
{
@@ -135,7 +140,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findBySql()}.
* This is the relational version of {@link CActiveRecord::findBySql()}.
*/
public function findBySql($sql,$params=array())
{
@@ -146,7 +151,7 @@ class CActiveFinder extends CComponent
}
/**
* This is relational version of {@link CActiveRecord::findAllBySql()}.
* This is the relational version of {@link CActiveRecord::findAllBySql()}.
*/
public function findAllBySql($sql,$params=array())
{
@@ -156,6 +161,16 @@ class CActiveFinder extends CComponent
return $baseRecords;
}
/**
* This is the relational version of {@link CActiveRecord::count()}.
* @since 1.0.3
*/
public function count($condition='',$params=array())
{
$criteria=$this->_builder->createCriteria($condition,$params);
return $this->_joinTree->count($criteria);
}
/**
* Finds the related objects for the specified active record.
* This method is internally invoked by {@link CActiveRecord} to support lazy loading.
@@ -303,7 +318,8 @@ class CJoinElement
if($this->_parent===null) // root element
{
$query=new CJoinQuery($this,$criteria);
$this->_finder->baseLimited=($criteria->offset>=0 || $criteria->limit>=0);
if($this->_finder->baseLimited===null)
$this->_finder->baseLimited=($criteria->offset>=0 || $criteria->limit>=0);
$this->buildQuery($query);
$this->runQuery($query);
}
@@ -345,7 +361,8 @@ class CJoinElement
{
$query->limit=$child->relation->limit;
$query->offset=$child->relation->offset;
$this->_finder->baseLimited=($query->offset>=0 || $query->limit>=0);
if($this->_finder->baseLimited===null)
$this->_finder->baseLimited=($query->offset>=0 || $query->limit>=0);
$query->groups[]=str_replace($child->relation->aliasToken.'.',$child->tableAlias.'.',$child->relation->group);
$query->havings[]=str_replace($child->relation->aliasToken.'.',$child->tableAlias.'.',$child->relation->having);
}
@@ -387,6 +404,37 @@ class CJoinElement
$child->find();
}
/**
* Count the number of primary records returned by the join statement.
* @param CDbCriteria the query criteria
* @return integer number of primary records.
* @since 1.0.3
*/
public function count($criteria=null)
{
$query=new CJoinQuery($this,$criteria);
// ensure only one big join statement is used
$this->_finder->baseLimited=false;
$this->_finder->joinAll=true;
$this->buildQuery($query);
if(is_string($this->_table->primaryKey))
{
$prefix=$this->getColumnPrefix();
$schema=$this->_builder->getSchema();
$column=$prefix.$schema->quoteColumnName($this->_table->primaryKey);
}
else if($criteria->select!=='*')
$column=$criteria->select;
else
throw new CDbException(Yii::t('yii','Unable to count records with composite primary keys. Please explicitly specify the SELECT option in the query criteria.'));
$query->selects=array("COUNT(DISTINCT $column)");
$query->orders=$query->groups=$query->havings=array();
$command=$query->createCommand($this->_builder);
return $command->queryScalar();
}
/**
* Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes.
* @param CJoinQuery the query being built up
@@ -854,6 +902,13 @@ class CJoinQuery
$this->conditions[]=$element->getCondition();
$this->orders[]=$element->getOrder();
$this->joins[]=$element->getJoinCondition();
if(is_array($element->relation->params))
{
if(is_array($this->params))
$this->params=array_merge($this->params,$element->relation->params);
else
$this->params=$element->relation->params;
}
$this->elements[$element->id]=true;
}

View File

@@ -576,6 +576,17 @@ abstract class CActiveRecord extends CModel
* Note, this is only honored by lazy loading, not eager loading.</li>
* <li>'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.</li>
* <li>'aliasToken': the column prefix for column reference disambiguation. Defaults to '??.'.</li>
* <li>'alias': the alias for the table associated with this relationship.
* This option has been available since version 1.0.1. It defaults to null,
* meaning the table alias is automatically generated. This is different
* from `aliasToken` in that the latter is just a placeholder and will be
* replaced by the actual table alias.</li>
* <li>'params': the parameters to be bound to the generated SQL statement.
* This should be given as an array of name-value pairs. This option has been
* available since version 1.0.3.</li>
* <li>'on': the ON clause. The condition specified here will be appended
* to the joining condition using the AND operator. This option has been
* available since version 1.0.2.</li>
* </ul>
*
* The following options are available for certain relations when lazy loading:
@@ -1543,6 +1554,12 @@ class CActiveRelation extends CComponent
* @var string WHERE clause. Column names referenced in the condition should be prefixed with '??.'.
*/
public $condition='';
/**
* @var array the parameters that are to be bound to the condition.
* The keys are parameter placeholder names, and the values are parameter values.
* @since 1.0.3
*/
public $params;
/**
* @var string ON clause. The condition specified here will be appended to the joining condition using AND operator.
* @since 1.0.2

View File

@@ -0,0 +1,213 @@
<?php
/**
* Message translations.
*
* This file is automatically generated by 'yiic message' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* NOTE, this file must be saved in UTF-8 encoding.
*
* @version $Id: $
*/
return array (
'"{path}" is not a valid directory.' => '',
'&lt; Previous' => '',
'&lt;&lt; First' => '',
'Active Record requires a "db" CDbConnection application component.' => '',
'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.' => '',
'Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.' => '',
'Alias "{alias}" is invalid. Make sure it points to an existing directory or file.' => '',
'Application base path "{path}" is not a valid directory.' => '',
'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => '',
'Authorization item "{item}" has already been assigned to user "{user}".' => '',
'CApcCache requires PHP apc extension to be loaded.' => '',
'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => '',
'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => '',
'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => '',
'CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbCache.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbCacheDependency.sql cannot be empty.' => '',
'CDbCommand failed to execute the SQL statement: {error}' => '',
'CDbCommand failed to prepare the SQL statement: {error}' => '',
'CDbConnection does not support reading schema for {driver} database.' => '',
'CDbConnection failed to open the DB connection: {error}' => '',
'CDbConnection is inactive and cannot perform any DB operations.' => '',
'CDbConnection.connectionString cannot be empty.' => '',
'CDbDataReader cannot rewind. It is a forward-only reader.' => '',
'CDbHttpSession.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbLogRoute requires database table "{table}" to store log messages.' => '',
'CDbLogRoute.connectionID "{id}" does not point to a valid CDbConnection application component.' => '',
'CDbMessageSource.connectionID is invalid. Please make sure "{id}" refers to a valid database application component.' => '',
'CDbTransaction is inactive and cannot perform commit or roll back operations.' => '',
'CDirectoryCacheDependency.directory cannot be empty.' => '',
'CFileCacheDependency.fileName cannot be empty.' => '',
'CFileLogRoute.logPath "{path}" does not point to a valid directory. Make sure the directory exists and is writable by the Web server process.' => '',
'CFilterChain can only take objects implementing the IFilter interface.' => '',
'CFlexWidget.baseUrl cannot be empty.' => '',
'CFlexWidget.name cannot be empty.' => '',
'CGlobalStateCacheDependency.stateName cannot be empty.' => '',
'CHttpCookieCollection can only hold CHttpCookie objects.' => '',
'CHttpRequest is unable to determine the entry script URL.' => '',
'CHttpRequest is unable to determine the path info of the request.' => '',
'CHttpRequest is unable to determine the request URI.' => '',
'CHttpSession.cookieMode can only be "none", "allow" or "only".' => '',
'CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.' => '',
'CHttpSession.savePath "{path}" is not a valid directory.' => '',
'CMemCache requires PHP memcache extension to be loaded.' => '',
'CMemCache server configuration must be an array.' => '',
'CMemCache server configuration must have "host" value.' => '',
'CMultiFileUpload.name is required.' => '',
'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => '',
'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => '',
'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => '',
'CSecurityManager.encryptionKey cannot be empty.' => '',
'CSecurityManager.validation must be either "MD5" or "SHA1".' => '',
'CSecurityManager.validationKey cannot be empty.' => '',
'CTypedList<{type}> can only hold objects of {type} class.' => '',
'CUrlManager.UrlFormat must be either "path" or "get".' => '',
'CXCache requires PHP XCache extension to be loaded.' => '',
'Cache table "{tableName}" does not exist.' => '',
'Cannot add "{child}" as a child of "{name}". A loop has been detected.' => '',
'Cannot add "{child}" as a child of "{parent}". A loop has been detected.' => '',
'Cannot add "{name}" as a child of itself.' => '',
'Cannot add an item of type "{child}" to an item of type "{parent}".' => '',
'Either "{parent}" or "{child}" does not exist.' => '',
'Error: Table "{table}" does not have a primary key.' => '',
'Error: Table "{table}" has a composite primary key which is not supported by crud command.' => '',
'Event "{class}.{event}" is attached with an invalid handler "{handler}".' => '',
'Event "{class}.{event}" is not defined.' => '',
'Failed to write the uploaded file "{file}" to disk.' => '',
'File upload was stopped by extension.' => '',
'Filter "{filter}" is invalid. Controller "{class}" does have the filter method "filter{filter}".' => '',
'Get a new code' => '',
'Go to page: ' => '',
'Invalid MO file revision: {revision}.' => '',
'Invalid MO file: {file} (magic: {magic}).' => '',
'Invalid enumerable value "{value}". Please make sure it is among ({enum}).' => '',
'Last &gt;&gt;' => '',
'List data must be an array or an object implementing Traversable.' => '',
'List index "{index}" is out of bound.' => '',
'Login Required' => '',
'Map data must be an array or an object implementing Traversable.' => '',
'Missing the temporary folder to store the uploaded file "{file}".' => '',
'Next &gt;' => '',
'No columns are being updated for table "{table}".' => '',
'No counter columns are being updated for table "{table}".' => '',
'Object configuration must be an array containing a "class" element.' => '',
'Please fix the following input errors:' => '',
'Property "{class}.{property}" is not defined.' => '',
'Property "{class}.{property}" is read only.' => '',
'Queue data must be an array or an object implementing Traversable.' => '',
'Relation "{name}" is not defined in active record class "{class}".' => '',
'Stack data must be an array or an object implementing Traversable.' => '',
'Table "{table}" does not have a column named "{column}".' => '',
'Table "{table}" does not have a primary key defined.' => '',
'The "filter" property must be specified with a valid callback.' => '',
'The "pattern" property must be specified with a valid regular expression.' => '',
'The "view" property is required.' => '',
'The CSRF token could not be verified.' => '',
'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.' => '',
'The active record cannot be deleted because it is new.' => '',
'The active record cannot be inserted to database because it is not new.' => '',
'The active record cannot be updated because it is new.' => '',
'The asset "{asset}" to be published does not exist.' => '',
'The column "{column}" is not a foreign key in table "{table}".' => '',
'The command path "{path}" is not a valid directory.' => '',
'The controller path "{path}" is not a valid directory.' => '',
'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.' => '',
'The file "{file}" is too large. Its size cannot exceed {limit} bytes.' => '',
'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.' => '',
'The file "{file}" was only partially uploaded.' => '',
'The first element in a filter configuration must be the filter class.' => '',
'The item "{name}" does not exist.' => '',
'The item "{parent}" already has a child "{child}".' => '',
'The layout path "{path}" is not a valid directory.' => '',
'The list is read only.' => '',
'The map is read only.' => '',
'The pattern for 12 hour format must be "h" or "hh".' => '',
'The pattern for 24 hour format must be "H" or "HH".' => '',
'The pattern for AM/PM marker must be "a".' => '',
'The pattern for day in month must be "F".' => '',
'The pattern for day in year must be "D", "DD" or "DDD".' => '',
'The pattern for day of the month must be "d" or "dd".' => '',
'The pattern for day of the week must be "E", "EE", "EEE", "EEEE" or "EEEEE".' => '',
'The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".' => '',
'The pattern for hour in AM/PM must be "K" or "KK".' => '',
'The pattern for hour in day must be "k" or "kk".' => '',
'The pattern for minutes must be "m" or "mm".' => '',
'The pattern for month must be "M", "MM", "MMM", or "MMMM".' => '',
'The pattern for seconds must be "s" or "ss".' => '',
'The pattern for time zone must be "z" or "v".' => '',
'The pattern for week in month must be "W".' => '',
'The pattern for week in year must be "w".' => '',
'The queue is empty.' => '',
'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". The foreign key does not point to either joining table.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".' => '',
'The requested controller "{controller}" does not exist.' => '',
'The requested view "{name}" is not found.' => '',
'The stack is empty.' => '',
'The system is unable to find the requested action "{action}".' => '',
'The system view path "{path}" is not a valid directory.' => '',
'The table "{table}" for active record class "{class}" cannot be found in the database.' => '',
'The value for the primary key "{key}" is not supplied when querying the table "{table}".' => '',
'The verification code is incorrect.' => '',
'The view path "{path}" is not a valid directory.' => '',
'Theme directory "{directory}" does not exist.' => '',
'This content requires the <a href="http://www.adobe.com/go/getflash/">Adobe Flash Player</a>.' => '',
'Unable to add an item whose name is the same as an existing item.' => '',
'Unable to change the item name. The name "{name}" is already used by another item.' => '',
'Unable to create application state file "{file}". Make sure the directory containing the file exists and is writable by the Web server process.' => '',
'Unable to find the decorator view "{view}".' => '',
'Unable to lock file "{file}" for reading.' => '',
'Unable to lock file "{file}" for writing.' => '',
'Unable to read file "{file}".' => '',
'Unable to replay the action "{object}.{method}". The method does not exist.' => '',
'Unable to write file "{file}".' => '',
'Unknown authorization item "{name}".' => '',
'Unrecognized locale "{locale}".' => '',
'View file "{file}" does not exist.' => '',
'Yii application can only be created once.' => '',
'You are not authorized to perform this action.' => '',
'Your request is not valid.' => '',
'{attribute} "{value}" has already been taken.' => '',
'{attribute} cannot be blank.' => '',
'{attribute} is invalid.' => '',
'{attribute} is not a valid URL.' => '',
'{attribute} is not a valid email address.' => '',
'{attribute} is not in the list.' => '',
'{attribute} is of the wrong length (should be {length} characters).' => '',
'{attribute} is too big (maximum is {max}).' => '',
'{attribute} is too long (maximum is {max} characters).' => '',
'{attribute} is too short (minimum is {min} characters).' => '',
'{attribute} is too small (minimum is {min}).' => '',
'{attribute} must be a number.' => '',
'{attribute} must be an integer.' => '',
'{attribute} must be repeated exactly.' => '',
'{attribute} must be {type}.' => '',
'{className} does not support add() functionality.' => '',
'{className} does not support delete() functionality.' => '',
'{className} does not support flush() functionality.' => '',
'{className} does not support get() functionality.' => '',
'{className} does not support set() functionality.' => '',
'{class} does not have a method named "{name}".' => '',
'{class} does not have attribute "{attribute}".' => '',
'{class} does not have attribute "{name}".' => '',
'{class} does not have relation "{name}".' => '',
'{class} does not support fetching all table names.' => '',
'{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.' => '',
'{class} must specify "model" and "attribute" or "name" property values.' => '',
'{class}.allowAutoLogin must be set true in order to use cookie-based authentication.' => '',
'{class}::authenticate() must be implemented.' => '',
'{controller} cannot find the requested view "{view}".' => '',
'{controller} contains improperly nested widget tags in its view "{view}". A {widget} widget does not have an endWidget() call.' => '',
'{controller} has an extra endWidget({id}) call in its view.' => '',
'{widget} cannot find the view "{view}".' => '',
);

View File

@@ -6,7 +6,7 @@
return array(
'sourcePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
'messagePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'messages',
'languages'=>array('zh_cn','zh_tw','de','es','sv','he','nl','pt','ru','it','fr','ja','pl','hu','ro','id'),
'languages'=>array('zh_cn','zh_tw','de','es','sv','he','nl','pt','ru','it','fr','ja','pl','hu','ro','id','vi','bg'),
'fileTypes'=>array('php'),
'exclude'=>array(
'.svn',

View File

@@ -0,0 +1,213 @@
<?php
/**
* Message translations.
*
* This file is automatically generated by 'yiic message' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* NOTE, this file must be saved in UTF-8 encoding.
*
* @version $Id: $
*/
return array (
'"{path}" is not a valid directory.' => '',
'&lt; Previous' => '',
'&lt;&lt; First' => '',
'Active Record requires a "db" CDbConnection application component.' => '',
'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.' => '',
'Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.' => '',
'Alias "{alias}" is invalid. Make sure it points to an existing directory or file.' => '',
'Application base path "{path}" is not a valid directory.' => '',
'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => '',
'Authorization item "{item}" has already been assigned to user "{user}".' => '',
'CApcCache requires PHP apc extension to be loaded.' => '',
'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => '',
'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => '',
'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => '',
'CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbCache.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbCacheDependency.sql cannot be empty.' => '',
'CDbCommand failed to execute the SQL statement: {error}' => '',
'CDbCommand failed to prepare the SQL statement: {error}' => '',
'CDbConnection does not support reading schema for {driver} database.' => '',
'CDbConnection failed to open the DB connection: {error}' => '',
'CDbConnection is inactive and cannot perform any DB operations.' => '',
'CDbConnection.connectionString cannot be empty.' => '',
'CDbDataReader cannot rewind. It is a forward-only reader.' => '',
'CDbHttpSession.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => '',
'CDbLogRoute requires database table "{table}" to store log messages.' => '',
'CDbLogRoute.connectionID "{id}" does not point to a valid CDbConnection application component.' => '',
'CDbMessageSource.connectionID is invalid. Please make sure "{id}" refers to a valid database application component.' => '',
'CDbTransaction is inactive and cannot perform commit or roll back operations.' => '',
'CDirectoryCacheDependency.directory cannot be empty.' => '',
'CFileCacheDependency.fileName cannot be empty.' => '',
'CFileLogRoute.logPath "{path}" does not point to a valid directory. Make sure the directory exists and is writable by the Web server process.' => '',
'CFilterChain can only take objects implementing the IFilter interface.' => '',
'CFlexWidget.baseUrl cannot be empty.' => '',
'CFlexWidget.name cannot be empty.' => '',
'CGlobalStateCacheDependency.stateName cannot be empty.' => '',
'CHttpCookieCollection can only hold CHttpCookie objects.' => '',
'CHttpRequest is unable to determine the entry script URL.' => '',
'CHttpRequest is unable to determine the path info of the request.' => '',
'CHttpRequest is unable to determine the request URI.' => '',
'CHttpSession.cookieMode can only be "none", "allow" or "only".' => '',
'CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.' => '',
'CHttpSession.savePath "{path}" is not a valid directory.' => '',
'CMemCache requires PHP memcache extension to be loaded.' => '',
'CMemCache server configuration must be an array.' => '',
'CMemCache server configuration must have "host" value.' => '',
'CMultiFileUpload.name is required.' => '',
'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => '',
'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => '',
'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => '',
'CSecurityManager.encryptionKey cannot be empty.' => '',
'CSecurityManager.validation must be either "MD5" or "SHA1".' => '',
'CSecurityManager.validationKey cannot be empty.' => '',
'CTypedList<{type}> can only hold objects of {type} class.' => '',
'CUrlManager.UrlFormat must be either "path" or "get".' => '',
'CXCache requires PHP XCache extension to be loaded.' => '',
'Cache table "{tableName}" does not exist.' => '',
'Cannot add "{child}" as a child of "{name}". A loop has been detected.' => '',
'Cannot add "{child}" as a child of "{parent}". A loop has been detected.' => '',
'Cannot add "{name}" as a child of itself.' => '',
'Cannot add an item of type "{child}" to an item of type "{parent}".' => '',
'Either "{parent}" or "{child}" does not exist.' => '',
'Error: Table "{table}" does not have a primary key.' => '',
'Error: Table "{table}" has a composite primary key which is not supported by crud command.' => '',
'Event "{class}.{event}" is attached with an invalid handler "{handler}".' => '',
'Event "{class}.{event}" is not defined.' => '',
'Failed to write the uploaded file "{file}" to disk.' => '',
'File upload was stopped by extension.' => '',
'Filter "{filter}" is invalid. Controller "{class}" does have the filter method "filter{filter}".' => '',
'Get a new code' => '',
'Go to page: ' => '',
'Invalid MO file revision: {revision}.' => '',
'Invalid MO file: {file} (magic: {magic}).' => '',
'Invalid enumerable value "{value}". Please make sure it is among ({enum}).' => '',
'Last &gt;&gt;' => '',
'List data must be an array or an object implementing Traversable.' => '',
'List index "{index}" is out of bound.' => '',
'Login Required' => '',
'Map data must be an array or an object implementing Traversable.' => '',
'Missing the temporary folder to store the uploaded file "{file}".' => '',
'Next &gt;' => '',
'No columns are being updated for table "{table}".' => '',
'No counter columns are being updated for table "{table}".' => '',
'Object configuration must be an array containing a "class" element.' => '',
'Please fix the following input errors:' => '',
'Property "{class}.{property}" is not defined.' => '',
'Property "{class}.{property}" is read only.' => '',
'Queue data must be an array or an object implementing Traversable.' => '',
'Relation "{name}" is not defined in active record class "{class}".' => '',
'Stack data must be an array or an object implementing Traversable.' => '',
'Table "{table}" does not have a column named "{column}".' => '',
'Table "{table}" does not have a primary key defined.' => '',
'The "filter" property must be specified with a valid callback.' => '',
'The "pattern" property must be specified with a valid regular expression.' => '',
'The "view" property is required.' => '',
'The CSRF token could not be verified.' => '',
'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.' => '',
'The active record cannot be deleted because it is new.' => '',
'The active record cannot be inserted to database because it is not new.' => '',
'The active record cannot be updated because it is new.' => '',
'The asset "{asset}" to be published does not exist.' => '',
'The column "{column}" is not a foreign key in table "{table}".' => '',
'The command path "{path}" is not a valid directory.' => '',
'The controller path "{path}" is not a valid directory.' => '',
'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.' => '',
'The file "{file}" is too large. Its size cannot exceed {limit} bytes.' => '',
'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.' => '',
'The file "{file}" was only partially uploaded.' => '',
'The first element in a filter configuration must be the filter class.' => '',
'The item "{name}" does not exist.' => '',
'The item "{parent}" already has a child "{child}".' => '',
'The layout path "{path}" is not a valid directory.' => '',
'The list is read only.' => '',
'The map is read only.' => '',
'The pattern for 12 hour format must be "h" or "hh".' => '',
'The pattern for 24 hour format must be "H" or "HH".' => '',
'The pattern for AM/PM marker must be "a".' => '',
'The pattern for day in month must be "F".' => '',
'The pattern for day in year must be "D", "DD" or "DDD".' => '',
'The pattern for day of the month must be "d" or "dd".' => '',
'The pattern for day of the week must be "E", "EE", "EEE", "EEEE" or "EEEEE".' => '',
'The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".' => '',
'The pattern for hour in AM/PM must be "K" or "KK".' => '',
'The pattern for hour in day must be "k" or "kk".' => '',
'The pattern for minutes must be "m" or "mm".' => '',
'The pattern for month must be "M", "MM", "MMM", or "MMMM".' => '',
'The pattern for seconds must be "s" or "ss".' => '',
'The pattern for time zone must be "z" or "v".' => '',
'The pattern for week in month must be "W".' => '',
'The pattern for week in year must be "w".' => '',
'The queue is empty.' => '',
'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". The foreign key does not point to either joining table.' => '',
'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".' => '',
'The requested controller "{controller}" does not exist.' => '',
'The requested view "{name}" is not found.' => '',
'The stack is empty.' => '',
'The system is unable to find the requested action "{action}".' => '',
'The system view path "{path}" is not a valid directory.' => '',
'The table "{table}" for active record class "{class}" cannot be found in the database.' => '',
'The value for the primary key "{key}" is not supplied when querying the table "{table}".' => '',
'The verification code is incorrect.' => '',
'The view path "{path}" is not a valid directory.' => '',
'Theme directory "{directory}" does not exist.' => '',
'This content requires the <a href="http://www.adobe.com/go/getflash/">Adobe Flash Player</a>.' => '',
'Unable to add an item whose name is the same as an existing item.' => '',
'Unable to change the item name. The name "{name}" is already used by another item.' => '',
'Unable to create application state file "{file}". Make sure the directory containing the file exists and is writable by the Web server process.' => '',
'Unable to find the decorator view "{view}".' => '',
'Unable to lock file "{file}" for reading.' => '',
'Unable to lock file "{file}" for writing.' => '',
'Unable to read file "{file}".' => '',
'Unable to replay the action "{object}.{method}". The method does not exist.' => '',
'Unable to write file "{file}".' => '',
'Unknown authorization item "{name}".' => '',
'Unrecognized locale "{locale}".' => '',
'View file "{file}" does not exist.' => '',
'Yii application can only be created once.' => '',
'You are not authorized to perform this action.' => '',
'Your request is not valid.' => '',
'{attribute} "{value}" has already been taken.' => '',
'{attribute} cannot be blank.' => '',
'{attribute} is invalid.' => '',
'{attribute} is not a valid URL.' => '',
'{attribute} is not a valid email address.' => '',
'{attribute} is not in the list.' => '',
'{attribute} is of the wrong length (should be {length} characters).' => '',
'{attribute} is too big (maximum is {max}).' => '',
'{attribute} is too long (maximum is {max} characters).' => '',
'{attribute} is too short (minimum is {min} characters).' => '',
'{attribute} is too small (minimum is {min}).' => '',
'{attribute} must be a number.' => '',
'{attribute} must be an integer.' => '',
'{attribute} must be repeated exactly.' => '',
'{attribute} must be {type}.' => '',
'{className} does not support add() functionality.' => '',
'{className} does not support delete() functionality.' => '',
'{className} does not support flush() functionality.' => '',
'{className} does not support get() functionality.' => '',
'{className} does not support set() functionality.' => '',
'{class} does not have a method named "{name}".' => '',
'{class} does not have attribute "{attribute}".' => '',
'{class} does not have attribute "{name}".' => '',
'{class} does not have relation "{name}".' => '',
'{class} does not support fetching all table names.' => '',
'{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.' => '',
'{class} must specify "model" and "attribute" or "name" property values.' => '',
'{class}.allowAutoLogin must be set true in order to use cookie-based authentication.' => '',
'{class}::authenticate() must be implemented.' => '',
'{controller} cannot find the requested view "{view}".' => '',
'{controller} contains improperly nested widget tags in its view "{view}". A {widget} widget does not have an endWidget() call.' => '',
'{controller} has an extra endWidget({id}) call in its view.' => '',
'{widget} cannot find the view "{view}".' => '',
);

View File

@@ -153,7 +153,7 @@ class CClientScript extends CApplicationComponent
if($html!=='')
{
$output=preg_replace('/(<body\b[^>]*>)/is','$1<###begin###>'.$html,$output,1,$count);
$output=preg_replace('/(<body\b[^>]*>)/is','$1<###begin###>',$output,1,$count);
if($count)
$output=str_replace('<###begin###>',$html,$output);
else

View File

@@ -32,6 +32,9 @@
* 'ips'=>array('127.0.0.1'),
* // optional, list of request types (case insensitive) that this rule applies to
* 'verbs'=>array('GET', 'POST'),
* // optional, a PHP expression whose value indicates whether this rule applies
* // This option is available since version 1.0.3.
* 'expression'=>'!$user->isGuest && $user->level==2',
* )
* </pre>
*
@@ -145,6 +148,12 @@ class CAccessRule extends CComponent
* @var array list of request types (e.g. GET, POST) that this rule applies to.
*/
public $verbs;
/**
* @var string a PHP expression whose value indicates whether this rule should be applied.
* In this expression, you can use <code>$user</code> which refers to <code>Yii::app()->user</code>.
* @since 1.0.3
*/
public $expression;
/**
@@ -161,7 +170,8 @@ class CAccessRule extends CComponent
&& $this->isUserMatched($user)
&& $this->isRoleMatched($user)
&& $this->isIpMatched($ip)
&& $this->isVerbMatched($verb))
&& $this->isVerbMatched($verb)
&& $this->isExpressionMatched($user))
return $this->allow ? 1 : -1;
else
return 0;
@@ -238,4 +248,16 @@ class CAccessRule extends CComponent
{
return empty($this->verbs) || in_array(strtolower($verb),$this->verbs);
}
/**
* @param IWebUser the user
* @return boolean the expression value. True if the expression is not specified.
* @since 1.0.3
*/
protected function isExpressionMatched($user)
{
if($this->expression===null)
return true;
return @eval('return '.$this->expression.';');
}
}

View File

@@ -41,7 +41,7 @@
* Note, when {@link allowAutoLogin cookie-based authentication} is enabled,
* all these persistent data will be stored in cookie. Therefore, do not
* store password or other sensitive data in the persistent storage. Instead,
* you should store them in session on the server side if needed.
* you should store them directly in session on the server side if needed.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @version $Id$
@@ -73,6 +73,65 @@ class CWebUser extends CApplicationComponent implements IWebUser
private $_keyPrefix;
/**
* PHP magic method.
* This method is overriden so that persistent states can be accessed like properties.
* @param string property name
* @return mixed property value
* @since 1.0.3
*/
public function __get($name)
{
if($this->hasState($name))
return $this->getState($name);
else
return parent::__get($name);
}
/**
* PHP magic method.
* This method is overriden so that persistent states can be set like properties.
* @param string property name
* @param mixed property value
* @since 1.0.3
*/
public function __set($name,$value)
{
if($this->hasState($name))
$this->setState($name,$value);
else
parent::__set($name,$value);
}
/**
* PHP magic method.
* This method is overriden so that persistent states can also be checked for null value.
* @param string property name
* @since 1.0.3
*/
public function __isset($name)
{
if($this->hasState($name))
return $this->getState($name)!==null;
else
return parent::__iset($name);
}
/**
* PHP magic method.
* This method is overriden so that persistent states can also be unset.
* @param string property name
* @throws CException if the property is read only.
* @since 1.0.3
*/
public function __unset($name)
{
if($this->hasState($name))
$this->setState($name,null);
else
parent::__unset($name);
}
/**
* Initializes the application component.
* This method overrides the parent implementation by starting session,
@@ -133,7 +192,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function getIsGuest()
{
return $this->getState('_id')===null;
return $this->getState('__id')===null;
}
/**
@@ -141,7 +200,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function getId()
{
return $this->getState('_id');
return $this->getState('__id');
}
/**
@@ -149,7 +208,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function setId($value)
{
$this->setState('_id',$value);
$this->setState('__id',$value);
}
/**
@@ -332,6 +391,18 @@ class CWebUser extends CApplicationComponent implements IWebUser
$_SESSION[$key]=$value;
}
/**
* Returns a value indicating whether there is a state of the specified name.
* @param string state name
* @return boolean whether there is a state of the specified name.
* @since 1.0.3
*/
public function hasState($key)
{
$states=$this->getState('__states',array());
return isset($states[$key]);
}
/**
* Clears all user identity information from persistent storage.
* The default implementation simply destroys the session.
@@ -411,7 +482,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
protected function saveIdentityStates()
{
$states=array();
foreach($this->getState('_states',array()) as $name)
foreach($this->getState('__states',array()) as $name=>$dummy)
$states[$name]=$this->getState($name);
return $states;
}
@@ -424,12 +495,16 @@ class CWebUser extends CApplicationComponent implements IWebUser
{
if(is_array($states))
{
$names=array();
foreach($states as $name=>$value)
{
$this->setState($name,$value);
$this->setState('_states',array_keys($states));
$names[$name]=true;
}
$this->setState('__states',$names);
}
else
$this->setState('_states',array());
$this->setState('__states',array());
}
/**

View File

@@ -0,0 +1,44 @@
<?php
/**
* Message translations.
*
* This file is automatically generated by 'yiic message' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* NOTE, this file must be saved in UTF-8 encoding.
*
* @version $Id: $
*/
return array (
'$_SERVER does not have {vars}.' => '',
'$_SERVER variable' => '',
'$_SERVER["SCRIPT_FILENAME"] must be the same as the entry script file path.' => '',
'APC extension' => '',
'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>' => '',
'DOM extension' => '',
'Either $_SERVER["REQUEST_URI"] or $_SERVER["QUERY_STRING"] must exist.' => '',
'GD extension' => '',
'Mcrypt extension' => '',
'Memcache extension' => '',
'PCRE extension' => '',
'PDO MySQL extension' => '',
'PDO PostgreSQL extension' => '',
'PDO SQLite extension' => '',
'PDO extension' => '',
'PHP 5.1.0 or higher is required.' => '',
'PHP version' => '',
'Reflection extension' => '',
'SOAP extension' => '',
'SPL extension' => '',
'This is required by encrypt and decrypt methods.' => '',
'This is required if you are using MySQL database.' => '',
'This is required if you are using PostgreSQL database.' => '',
'This is required if you are using SQLite database.' => '',
'Unable to determine URL path info. Please make sure $_SERVER["PATH_INFO"] (or $_SERVER["PHP_SELF"] and $_SERVER["SCRIPT_NAME"]) contains proper value.' => '',
);

View File

@@ -6,7 +6,7 @@
return array(
'sourcePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
'messagePath'=>dirname(__FILE__),
'languages'=>array('zh_cn','zh_tw','de','es','sv','he','nl','pt','ru','it','fr','ja','pl','hu','ro','id'),
'languages'=>array('zh_cn','zh_tw','de','es','sv','he','nl','pt','ru','it','fr','ja','pl','hu','ro','id','vi','bg'),
'fileTypes'=>array('php'),
'translator'=>'t',
'exclude'=>array(

View File

@@ -0,0 +1,44 @@
<?php
/**
* Message translations.
*
* This file is automatically generated by 'yiic message' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* NOTE, this file must be saved in UTF-8 encoding.
*
* @version $Id: $
*/
return array (
'$_SERVER does not have {vars}.' => '',
'$_SERVER variable' => '',
'$_SERVER["SCRIPT_FILENAME"] must be the same as the entry script file path.' => '',
'APC extension' => '',
'All <a href="http://www.yiiframework.com/doc/api/#system.db">DB-related classes</a>' => '',
'DOM extension' => '',
'Either $_SERVER["REQUEST_URI"] or $_SERVER["QUERY_STRING"] must exist.' => '',
'GD extension' => '',
'Mcrypt extension' => '',
'Memcache extension' => '',
'PCRE extension' => '',
'PDO MySQL extension' => '',
'PDO PostgreSQL extension' => '',
'PDO SQLite extension' => '',
'PDO extension' => '',
'PHP 5.1.0 or higher is required.' => '',
'PHP version' => '',
'Reflection extension' => '',
'SOAP extension' => '',
'SPL extension' => '',
'This is required by encrypt and decrypt methods.' => '',
'This is required if you are using MySQL database.' => '',
'This is required if you are using PostgreSQL database.' => '',
'This is required if you are using SQLite database.' => '',
'Unable to determine URL path info. Please make sure $_SERVER["PATH_INFO"] (or $_SERVER["PHP_SELF"] and $_SERVER["SCRIPT_NAME"]) contains proper value.' => '',
);

View File

@@ -14,34 +14,30 @@ If you have business inquries or other questions, please fill out the following
<div class="yiiForm">
<p>
Fields with <span class="required">*</span> are required.
</p>
<?php echo CHtml::form(); ?>
<?php echo CHtml::errorSummary($contact); ?>
<div class="simple">
<?php echo CHtml::activeLabelEx($contact,'name'); ?>
<?php echo CHtml::activeLabel($contact,'name'); ?>
<?php echo CHtml::activeTextField($contact,'name'); ?>
</div>
<div class="simple">
<?php echo CHtml::activeLabelEx($contact,'email'); ?>
<?php echo CHtml::activeLabel($contact,'email'); ?>
<?php echo CHtml::activeTextField($contact,'email'); ?>
</div>
<div class="simple">
<?php echo CHtml::activeLabelEx($contact,'subject'); ?>
<?php echo CHtml::activeLabel($contact,'subject'); ?>
<?php echo CHtml::activeTextField($contact,'subject',array('size'=>60,'maxlength'=>128)); ?>
</div>
<div class="simple">
<?php echo CHtml::activeLabelEx($contact,'body'); ?>
<?php echo CHtml::activeLabel($contact,'body'); ?>
<?php echo CHtml::activeTextArea($contact,'body',array('rows'=>6, 'cols'=>50)); ?>
</div>
<?php if(extension_loaded('gd')): ?>
<div class="simple">
<?php echo CHtml::activeLabelEx($contact,'verifyCode'); ?>
<?php echo CHtml::activeLabel($contact,'verifyCode'); ?>
<div>
<?php $this->widget('CCaptcha'); ?>
<?php echo CHtml::activeTextField($contact,'verifyCode'); ?>

View File

@@ -3,20 +3,17 @@
<h1>Login</h1>
<div class="yiiForm">
<p>
Fields with <span class="required">*</span> are required.
</p>
<?php echo CHtml::form(); ?>
<?php echo CHtml::errorSummary($form); ?>
<div class="simple">
<?php echo CHtml::activeLabelEx($form,'username'); ?>
<?php echo CHtml::activeLabel($form,'username'); ?>
<?php echo CHtml::activeTextField($form,'username') ?>
</div>
<div class="simple">
<?php echo CHtml::activeLabelEx($form,'password'); ?>
<?php echo CHtml::activeLabel($form,'password'); ?>
<?php echo CHtml::activePasswordField($form,'password') ?>
<p class="hint">
Hint: You may login with <tt>demo/demo</tt> or <tt>admin/admin</tt>.
@@ -25,7 +22,7 @@ Hint: You may login with <tt>demo/demo</tt> or <tt>admin/admin</tt>.
<div class="action">
<?php echo CHtml::activeCheckBox($form,'rememberMe'); ?>
<?php echo CHtml::activeLabelEx($form,'rememberMe'); ?>
<?php echo CHtml::activeLabel($form,'rememberMe'); ?>
<br/>
<?php echo CHtml::submitButton('Login'); ?>
</div>

View File

@@ -573,4 +573,16 @@ class CActiveRecord2Test extends CTestCase
$this->assertEquals($friends[0]->id,2);
$this->assertEquals($friends[1]->id,3);
}
public function testRelationalCount()
{
$count=Post2::model()->with('author','firstComment','comments','categories')->count();
$this->assertEquals(5,$count);
$count=Post2::model()->with('author','firstComment','comments','categories')->count('posts.id=4');
$this->assertEquals(1,$count);
$count=Post2::model()->with('author','firstComment','comments','categories')->count('posts.id=14');
$this->assertEquals(0,$count);
}
}

View File

@@ -627,4 +627,16 @@ class CActiveRecordTest extends CTestCase
$this->assertEquals(array(),$post->comments);
$this->assertEquals(array(),$post->categories);
}
public function testRelationalCount()
{
$count=Post::model()->with('author','firstComment','comments','categories')->count();
$this->assertEquals(5,$count);
$count=Post::model()->with('author','firstComment','comments','categories')->count('posts.id=4');
$this->assertEquals(1,$count);
$count=Post::model()->with('author','firstComment','comments','categories')->count('posts.id=14');
$this->assertEquals(0,$count);
}
}