[原创中文翻译]symfony askeet24:第五天,表单和页面。
October 2, 2007 – 7:54 pm[欢迎转载,转载请注名出处http://symfony.net.cn/。本文英文版权归symfony官方网站所有]
Login form
login表单
There are users in the test data, but no way for the application to recognize one. Let’s give access to a login form from every page of the application. Open the global layout askeet/apps/frontend/templates/layout.php and add in the following line before the link to about:
我们导入的测试数据里含有用户信息,但我们的程序没有涉及到用户。现在让我们加入登录表单。打开全局布局页面(askeet/apps/frontend/templates/layout.php),加几行代码:
<li><?php echo link_to('sign in', 'user/login') ?></li>
The current layout places this link just behind the web debug toolbar. To see it, fold the toolbar by clicking its ‘Sf’ icon.
当前layout布局把链接放到web debug工具条后面了。想看到的话,先折叠工具条,点sf图标。
It is time to create the user module. While the question module was generated during day two, this time we will just ask symfony to create the module skeleton, and we will write the code ourselves.
该创建用户模块了。前两天生成了问题模块,这次我们让symfony创建模块骨架,然后我们自己来完善代码。
$ symfony init-module frontend user
The skeleton contains a default index action and an indexSuccess.php template. Get rid of both, since we won’t need them.
骨架包括一个默认的index动作和一个indexSuccess.php模板。里面的代码都删除掉,我们都不需要。
Create the user/login action
建立user/login动作
In the user/actions/action.class.php file (under the new askeet/apps/frontend/modules/ directory), add the following login action:
在user/actions/action.class.php(askeet/apps/frontend/modules/下),照下面这样增加login动作:
public function executeLogin()
{
$this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());
return sfView::SUCCESS;
}
The action saves the referrer in a request attribute. It will then be available to the template to be put in a hidden field, so that the target action of the form can redirect to the original referer after a successful login.
动作把referrer做为一个request属性。放在模板的隐藏域里,成功login后,form的目标动作会重定向到referer,而且referer也会显示。
The return sfView::SUCCESS passes the result of the action to the loginSuccess.php template. This statement is implied in actions that don’t contain a return statement, that’s why the default template of an action is called actionnameSuccess.php.
return sfView::SUCCESS把动作的结果传递给loginSuccess.php模板。代码被包含在动作里,没有包含常见的那种return代码,这就是为什么默认动作的模板被叫做actionnameSuccess.php。
[译者:这里如果有疑问,请去看看symfony API手册。]
Before working more on the action, let’s have a look at the template.
在处理动作前,让我们看看模板。
Create the loginSuccess.php template
创建LoginSuccess.php模板
Many human-computer interactions on the web use forms, and symfony facilitates the creation and the management of forms by providing a set of form helpers.
大家上网都要用到表单,symfony提供了form helper帮助你轻松创建和管理表单。
In the askeet/apps/frontend/modules/user/templates/ directory, create the following loginSuccess.php template:
在askeet/apps/frontend/modules/user/templates/下,建立loginSuccess.php模板,输入下面代码:
<?php echo form_tag('user/login') ?>
<fieldset>
<div class="form-row">
<label for="nickname">nickname:</label>
<?php echo input_tag('nickname', $sf_params->get('nickname')) ?>
</div>
<div class="form-row">
<label for="password">password:</label>
<?php echo input_password_tag('password') ?>
</div>
</fieldset>
<?php echo input_hidden_tag('referer', $sf_request->getAttribute('referer')) ?>
<?php echo submit_tag('sign in') ?>
</form>
This template is your first introduction to the form helpers. These symfony functions help to automate the writing of form tags. The form_tag() helper opens a form with a default POST behaviour, and points to the action given as argument. The input_tag() helper produces an <input> tag (that’s a surprise) by automatically adding an id attribute based on the name given as first argument; the default value is taken from the second argument. You can find more about form helpers and the HTML code they generate in the related chapter of the symfony book.
从这个模板开始你的form helper之旅。symfony帮助你自动书写这些表单标签。form_tag() helper默认使用POST方式,按照我们写的参数转到对应动作处理。input_tag() helper提供了一个<input>标签(很惊奇吧),自动把属性id作为第一个参数;第二个参数代表默认值。更多内容请看symfony book。
The essential thing here is that the action called when the form is submitted (the argument of form_tag()) is the same login action used to display it. So let’s go back to the action.
这里最本质的是当表单被提交时,转到处理动作(form_tag()的参数),我们都是使用了同样的动作,即动作既用来显示表单,又用来处理表单提交的数据。现在回去修改一下动作。
Handle the login form submission
处理login表单提交的数据
Replace the login action that we just wrote with the following code:
把login动作的代码替换为:
public function executeLogin()
{
if ($this->getRequest()->getMethod() != sfRequest::POST)
{
// display the form
$this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());
}
else
{
// handle the form submission
$nickname = $this->getRequestParameter('nickname');
$c = new Criteria();
$c->add(UserPeer::NICKNAME, $nickname);
$user = UserPeer::doSelectOne($c);
// nickname exists?
if ($user)
{
// password is OK?
if (true)
{
$this->getUser()->setAuthenticated(true);
$this->getUser()->addCredential(’subscriber’);
$this->getUser()->setAttribute(’subscriber_id’, $user->getId(), ’subscriber’);
$this->getUser()->setAttribute(’nickname’, $user->getNickname(), ’subscriber’);
// redirect to last page
return $this->redirect($this->getRequestParameter(’referer’, ‘@homepage’));
}
}
}
}
The login action will be used both to display the login form and to process it. In consequence, it has to know in which context it is called. If the action is not called in POST mode, it is because it is requested from a link: That’s the previous case we talked about earlier. If the request is in POST mode, theon is called from a form and it is time to handle it.
login动作被用来既显示login表单,又处理表单提交的数据。那么,它必须知道是显示呢,还是处理数据呢。如果链接请求不是POST方式,那么就是前面的情况——显示login表单。如果是POST方式,就处理表单提交过来的数据。
[译者:在这里说几句。上面这段代码( if ($this->getRequest()->getMethod() != sfRequest::POST)),无非就是把两个动作合成一个来写了。我们以前做项目,可能处理登录之类的功能会简单得使用这样的方法,一个动作转到登录模板页面上;另一个动作处理登录模板传送过来的数据。symfony里提供的解决思路是判断来路是不是post方式,如果不是,转到登录模板,如果是,处理数据。连动作都是能少写就少写,askeet真是够精致!]
The action gets the value of the nickname field from the request parameters, and requires the User table to see if this user exists in the database.
动作从模板提交过来的数据中获得nickname的值,对User表进行查询查看用户是否在数据库中。
Then there will be, in the near future, a control of the password that will grant credentials to the user. For now, the only thing this action does is to store in a session attribute the id and the nickname of the user. Eventually, the action redirects to the original referer thanks to the hidden referer field in the form, passed as a request parameter. If this field is empty, the default value (@homepage, which is the routing rule name for question/list) is used instead.
随着项目的完善,将会有权限密码提供给用户。现在,动作唯一做的事是把用户的id和nickname写到session里。最后,靠表单里的隐藏域来传递请求参数,动作能重定向到原来的referer。如果不存在,默认值(@homepage,就是question/list的路由名字)被使用。
Notice the difference between the two types of attributes set in this example: The request attributes ($this->getRequest()->setAttribute()) are held for the template and forgotten as soon as the answer is sent to the referrer. The session attributes ($this->getUser()->setAttribute()) are kept during the life of the user’s session, and other actions will be able to access them again in the future. If you want to know more about attributes, you should have a look at the parameter holder chapter of the symfony book.
注意例子里的两种set属性的不同:request属性($this->getRequest()->setAttribute())被放在模板里而且答案一发送到referrer就删除。session属性($this->getUser()->setAttribute())具有和用户session一样的生命期,其他动作可以继续访问。如果想知道更多,看symfony book。
Grant privileges
授权特殊权限
It is a good thing that users can log in to the askeet website, but they won’t do it just for fun. Login will be required to post a new question, to declare interest about a question, and to rate a comment. All the other actions wiil be open to non logged users.
现在用户可以登入askeet网站了,但不能仅仅为了好玩而登录。登录后会被要求发表一个新问题,表达对哪些问题感兴趣和发表评论。所有其他的action也都会向未登录用户开放。
To set a user as authenticated, you need to call the ->setAuthenticated() method of the sfUser object. This object also provides a credentials mechanism (->addCredential()), to refine access restriction according to profiles. The user credentials chapter of the symfony book explains all that in detail.
把一个用户设置为权限用户,需要调用sfUser对象的->setAuthenticated()。此对象也提供了可信任的原理(->addCredential()),根据权限制订约束条件。symfony book有详细介绍。
That’s the purpose of the two lines:
两行代码的用途:
$this->getContext()->getUser()->setAuthenticated(true);
$this->getContext()->getUser()->addCredential('subscriber');
When the nickname is recognized, not only will the user data put in session attributes, but the user will also be granted access to restricted parts of the site. We’ll see tomorrow how to restrict access of some parts of the application to authenticated users.
当nickname被认出后,不仅用户数据被写入了session,而且用户也算是进入了网站的受限制部分。我们明天看看怎么对授权用户给部分程序加限制。
Add the user/logout action
增加user/logout动作
There is one last trick about the ->setAttribute() method: The last argument (subscriber in the above example) defines the namespace where the attribute will be stored. Not only does a namespace allow a name already existing in another namespace to be given to an attribute, it also allows the quick removal of all its attributes with a single command:
有一个关于->setAttribute()方法的技巧:最后的参数(subscriber,见上面的例子)定义了namespace,存储属性。不仅namespace允许name已经存在于另外的namespace,并被赋予一个属性,也允许用简单命令快速移动所有属性:
public function executeLogout()
{
$this->getUser()->setAuthenticated(false);
$this->getUser()->clearCredentials();
$this->getUser()->getAttributeHolder()->removeNamespace(’subscriber’);
$this->redirect(‘@homepage’);
}
Using namespaces saved us from removing the two attributes one by one: That’s one less line of code. Talk about laziness!
用namespace一个一个让我们删除这两个属性:这不止一行代码。讨论一下吧。
Update the layout
更新layout布局
The layout still shows a ‘login’ link even if a user is already logged. Let’s quickly fix it. In askeet/apps/frontend/templates/layout.php, change the line that we just added at the beginning of today’s tutorial with:
用户已经登入完毕了,layout布局页面仍然展示一个“login”链接。快修正一下吧。askeet/apps/frontend/templates/layout.php,改一下今天教程前面我们添加的代码:
<?php if ($sf_user->isAuthenticated()): ?>
<li><?php echo link_to('sign out', 'user/logout') ?></li>
<li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>
<?php else: ?>
<li><?php echo link_to('sign in/register', 'user/login') ?></li>
<?php endif ?>
It is time to test all this by displaying any page of the application, clicking the ‘login’ link, entering a valid nickname (’anonymous’ should do the trick) and validating it. If the ‘login’ link on top of the window changes to ’sign out’, you did everything right. Eventually, try to logout to check if the ‘login’ links appears again.
是时候测试所有程序显示页面了,点“login”链接,输入用户名(匿名不行)验证一下。如果上面的“login”变成了“sign out”,你就做对了。最后,试试logout看看“login”是不是又出现了。
You will find more information about the manipulation of user session attributes in the user session chapter of the symfony book.
去symfony book查看更多关于会话的知识吧。
Question pager
Question页面
As thousands of symfony enthusiasts will rush to the askeet site, it is very probable that the list of questions displayed in the home page will grow very long. To avoid slow requests and excessive scrolling, it is necessary to paginate the list of questions.
正像成千上万的symfony爱好者涌入askeet网站那样,不断增加的问题会让问题列表在首页变得非常长。避免过慢的请求和过长的滚动条,必须给问题列表标记页数。
[译者:到分页功能了。]
Symfony provides an object just for that purpose: The sfPropelPager. It encapsulates the request to the database so that only the records to display on the current page are required. For instance, if a pager is initialized to display 10 records per page, the request to the database will be limited to 10 results, and the offset set to match the page rank.
symfony为此功能提供了一个对象:sfPropelPage。它封装了一个对数据库的请求,只取出当前页面需要显示的数据。例如,如果页面被设计为一页只显示10个记录,对数据库的请求会被限制在只取出10条纪录。
Modify the question/list action
修改question/list动作
During day three, we saw that the list action of the question module was quite succinct:
在前三天的教程里,我们的list动作和question模块很简洁:
public function executeList ()
{
$this->questions = QuestionPeer::doSelect(new Criteria());
}
We are going to modify this action to pass a sfPropelPager object to the template instead of an array. In the same time, we are going to order the questions by number of interests:
我们修改这个动作,用sfPropelPage对象传递数据到模板,而不用数组了。同时,我们根据interest数量来获取question:
public function executeList ()
{
$pager = new sfPropelPager('Question', 2);
$c = new Criteria();
$c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS);
$pager->setCriteria($c);
$pager->setPage($this->getRequestParameter('page', 1));
$pager->setPeerMethod('doSelectJoinUser');
$pager->init();
$this->question_pager = $pager;
}
The initialization of the sfPropelPager object specifies which class of object it will contain, and the maximum number of objects that can be put in a page (two in this example). The ->setPage() method uses a request parameter to set the current page. For instance, if this page parameter has a value of 2, the sfPropelPager will return the results 3 to 5. The default value of the page request parameter being 1, this pager will return the results 1 to 2 by default. You will find more information about the sfPropelPager object and its methods in the pager chapter of the symfony book.
初始化过的sfPropelPager对象很详细,包含哪个对象的类都很详细,对象的最大数量会被放到页面上(例子里是2)。->setPage()方法用了请求参数来设置当前页。例如,如果页面的参数初始值是2,sfPropelPager会返回结果3到5。页面默认参数是1,页面会返回结果1到2。在symfony book里有更多关于sfPropelPager对象知识。
Use a custom parameter
使用自定义参数
It is always a good idea to put the constants that you use in configuration files. For instance, the number of results per page (2 in this example) could be replaced by a parameter, defined in your custom application configuration. Change the new sfPropelPager line above by:
最好把用到的常量都写到配置文件里。例如,每个页面的数量(例子是2),可以被一个参数代替,在你的自定义程序配置文件里定义。把上面我们刚写的sfPropelPager代码改改:
...
$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
Open the custom application configuration file (askeet/apps/frontend/config/app.yml) and add in:
打开自定义程序配置文件(askeet/apps/frontend/config/app.yml):
all:
pager:
homepage_max: 2
The pager key here is used as a namespace, that’s why it also appears in the parameter name. You will find more about custom configuration and the rules to name custom parameters in the configuration chapter of the symfony book.
页面常量用来当作namespace,参数名中显示。你会发现更多的自定义配置文件和自定义变量规定,都在symfony book里。
Modify the listSuccess.php template
修改listSuccess.php模板
In the listSuccess.php template, just replace the line
在listSuccess.php,替换掉这行
<?php foreach($questions as $question): ?>
by
为下面代码
<?php foreach($question_pager->getResults() as $question): ?>
so that the page displays the list of results stored in the pager.
页面就显示结果了。
Add page navigation
增加页面导航
There is one more thing to add to this template: The page navigation. For now, all that the template does is display the first two questions, but we should add the ability to go to the next page, and then to go back to the previous page. To do that, append at the end of the template:
我们还需要把这个添加到模板上——页面导航。现在,所有的模板都显示两个问题,我们还需要加上分页,可以访问下一页,返回上一页。为了实现这些,在模板后面加上这些:
<div id="question_pager">
<?php if ($question_pager->haveToPaginate()): ?>
<?php echo link_to('«', 'question/list?page=1') ?>
<?php echo link_to('<', 'question/list?page='.$question_pager->getPreviousPage()) ?>
<?php foreach ($question_pager->getLinks() as $page): ?>
<?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?>
<?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?>
<?php endforeach; ?>
<?php echo link_to('>', 'question/list?page='.$question_pager->getNextPage()) ?>
<?php echo link_to('»', 'question/list?page='.$question_pager->getLastPage()) ?>
<?php endif; ?>
</div>
This code takes advantage of the numerous methods of the sfPropelPager object, among which ->haveToPaginate(), which returns true only if the number of results to the request exceeds the page size; ->getPreviousPage(), ->getNextPage() and ->getLastPage(), which have obvious meanings; ->getLinks(), which provides an array of page numbers; and ->getCurrentMaxLink(), which returns the last page number.
代码利用了sfPropelPager对象计算方法,在->haveToPaginate(),在数据库里记录数量超过页面设置的每页显示数量后返回真,即显示分页链接;->getPreviousPage(),->getNextPage()和->getLastPage(),文字上能很明显看出来实现的功能;->getLinks(),给出了包括页面数量的数组;->getCurrentMaxLink(),返回最后页面。
This example also shows one handy symfony link helper: link_to_unless() will output a regular link_to() if the test given as the first argument is false, otherwise the text will be output without a link, enclosed in a simple <span>.
例子也展示了方便的symfony link helper:如果测试第一个参数失败,即不符合if条件,link_to_unless()将输出一个规则link_to(),否则文字会被以无链接属性而输出,加上<span>标签。
Did you test the pager? You should. The modification isn’t over until you validate it with your own eyes. To do that, just open the test data file created during day three, and add a few questions for the page navigation to appear. Relaunch the import data batch and request the homepage again. Voila.
你测试过页面吗?现在可以测试。眼见为实,证明你修改的程序是正确的。为了做好测试,打开第三天创建的测试数据文件,为导航页面加几个问题。重新载入批处理程序,打开首页。
Add a routing rule for the subsequent pages
为后面的页添加路由规则
By default, the urls of the pages will look like:
默认的URL看起来是这样:
http://askeet/frontend_dev.php/question/list/page/XX
Let’s take advantage of the routing rules to have those pages understand:
利用路由规则让这些地址变成下面这样:
http://askeet/frontend_dev.php/index/XX
Just open the apps/frontend/config/routing.yml file and add at the top:
打开apps/frontend/config/routing.yml在顶部加上:
popular_questions:
url: /index/:page
param: { module: question, action: list }
While we are at it, add another routing rule for the login page:
趁热打铁,同时为登录页加个路由规则:
login:
url: /login
param: { module: user, action: login }
Refactoring
整理程序
Model
整理模型
The question/list action executes code that is closely related to the model, that’s why we will move this code to the model. Replace the question/list action by:
问题模型里的list动作执行代码,有点类似lib/model里的功能,我们为什么不把这些动作封装到lib/model里呢?把question/list动作里的代码替换成:
public function executeList ()
{
$this->question_pager = QuestionPeer::getHomepagePager($this->getRequestParameter('page', 1));
}
…and add the following method to the QuestionPeer.php class in lib/model:
… …在lib/model下给QuestionPeer.php文件增加几个函数:
public static function getHomepagePager($page)
{
$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
$c = new Criteria();
$c->addDescendingOrderByColumn(self::INTERESTED_USERS);
$pager->setCriteria($c);
$pager->setPage($page);
$pager->setPeerMethod('doSelectJoinUser');
$pager->init();
return $pager;
}
The same idea applies to the question/show action, written yesterday: The use of Propel objects to retrieve a question from its stripped title should belong to the model. So change the question/show action by:
封装到lib/model里的方法同样适用于question/show动作,昨天写过了:Propel根据模块的stripped title从数据库中取出问题。所以改question/show动作为:
public function executeShow()
{
$this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));
$this->forward404Unless($this->question);
}
Add to QuestionPeer.php:
加到QuestionPeer.php:
public static function getQuestionFromTitle($title)
{
$c = new Criteria();
$c->add(QuestionPeer::STRIPPED_TITLE, $title);
return self::doSelectOne($c);
}
Templates
整理模板
The list of question displayed in question/templates/listSuccess.php will be reused somewhere else in the future. So we will put the template code to display a list of question in a _list.php fragment and replace the listSuccess.php content by a simple:
模板question/templates/listSuccess.php显示的question列表功能以后会被重用。所以我们把模板代码放在在_list.php代码片断中(还记得前面的_interested_user.php代码片断吗?)来显示question列表,替换listSuccess.php内容为:
<h1>popular questions</h1>
<?php echo include_partial('list', array('question_pager' => $question_pager)) ?>
The content of the _list.php fragment can be seen in the askeet SVN repository.
_list.php代码片断的具体代码可以在askeet SVN里找到。
[译者,2007年10月16日下午3点40:如果你从askeet的SVN上下载了那个_question_list.php文件后,除了改一下文件名外,还需要把其中的四行代码改一下。在这里我把那些代码贴出来吧。
就是把answer div部分的代码换了(粗体斜体部分是需要修改的代码),而且前面的部分代码要注释掉一行:
首先,替换<?php use_helpers(’Text’,'Question’) ?> 这行代码为:<?php use_helper(’Text’) ?> ,因为我们没有写QuestionHelper.php这样的文件;
然后,接着修改answer div部分代码:
<div id=”question_pager”>
<?php if ($question_pager->haveToPaginate()): ?>
<?php echo link_to(’«’, ‘question/list?page=1′) ?>
<?php echo link_to(’<’, ‘question/list?page=’.$question_pager->getPreviousPage()) ?>
<?php foreach ($question_pager->getLinks() as $page): ?>
<?php echo link_to_unless($page == $question_pager->getPage(), $page, ‘question/list?page=’.$page) ?>
<?php echo ($page != $question_pager->getCurrentMaxLink()) ? ‘-’ : ” ?>
<?php endforeach ?>
<?php echo link_to(’>’, ‘question/list?page=’.$question_pager->getNextPage()) ?>
<?php echo link_to(’»’, ‘question/list?page=’.$question_pager->getLastPage()) ?>
<?php endif ?>
</div>
]


7 Responses to “[原创中文翻译]symfony askeet24:第五天,表单和页面。”
感谢HE LUAN的资料。
刚刚翻译了这个教程的3,4发现HE LUAN已经把所有的都翻译好了,实在是非常感谢。
关于 我也遇到了问题,
1,找不到 use_helpers,据说是以前的版本有这个函数,后来都统一成了 use_helper了,也就是说,use_helper可以加多个助手了。
所以,修改 use_helpers 为 use_helper
2,Question助手找不到
教程里么有提到制作了 QuestionHelper.php,但是我在
http://svn.askeet.com/tags/release_day_5/frontend/lib/helper/
找到了Question和 Answer的Helper,下载下来放到 frontend/lib/helper/
下,http://askeet/frontend_dev.php/question/list/page/1
就可以正常显示了。
QuestionHelper.php做了一件事,就是把
定义函数question_pager_link ,完成link_to的工作。
By lujiajian on Dec 5, 2007
He Luan:请教一个问题,在最后重构模板完成之后,清空缓存,刷新页面,这时页面无法打开,提示如下错误:
Fatal error: Unsupported operand types in D:\xampp\php\PEAR\symfony\util\Spyc.class.php on line 667
在symfony 官方网站上找了许久也没有找到解决办法,不知道你们有没有遇到过?
谢谢!
By Jerry li on Oct 26, 2008
Jerry Li的这个问题我没有碰到过,我建议你对这个问题不要在symfony官方网站上找了,这是个php的问题,不是框架的问题,祝你好运!
By He Luan on Oct 28, 2008
问题解决了,估计是有2个原因引起的。一是yml文件中少了一个空格,其次可能是template文件中不小心把单引号输成了了全角符号。很佩服你的毅力,把24篇文章全部译完了,精神可嘉!
不过要打击你一下,呵呵,仔细看的话译文中有一些词语有待商榷,另外也出现了一些错误。比如Refactoring应该译为“重构”,等等。这篇文章最后的示例代码中”>”应该是” ”,其它的几个符号也应该用转义后的字符串,否则会出错。
By Jerry Li on Oct 29, 2008
是你的blog把转移字符串给过滤了,我说呢,这回看能不能显示““ ””
By Jerry Li on Oct 29, 2008
还是不能。没辙了…..
By Jerry Li on Oct 29, 2008
呵呵,这个是我的疏忽,当时我光在symfony群里告诉大家了,不要复制我的blog上的代码,因为wordpress把许多东西都过滤了… …如果要复制代码,建议去symfony官方网站复制。
至于翻译文字质量出现问题,真的是难免的,我的水平有限,在这里欢迎大家帮我校译,谢谢!
By He Luan on Oct 30, 2008