[原创中文翻译]symfony askeet24:第二十天,增加管理员和版主。

October 10, 2007 – 2:23 am

[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]

The expected result: what the client says

需求分析:客户的要求是什么

Today’s job will consist of a few new actions, new templates and new model method, and we already know how to do that. The hardest part is probably to define what is needed, and where to put it. It is both a functional and usability concern, and it’s a good thing that developers focus on something else than code every once in a while.

今天的工作包含创建一些新动作、新模板和新模型方法,我们都知道该怎么做了。设计中最难的部分是定义项目怎么做,在那里需要实现某些功能,在那里不需要实现某些功能。方法和可用的组件、开发者的代码都必须结合起来。

(译者:这里确实该好好看看,项目的前期定义是非常重要的。这里一点一点讲解了askeet后台的定义内容,看起来一目了然。弄明白了再去开发程 序,绝对不会出现功能上的遗漏和错误。可惜的是,现阶段中国的软件科技公司很少有做得比较好的,是这个东西不重要么?不是,要不然为什么国内、国外只要涉 及软件的书都写这个重要呢;是项目时间紧张么?不是,没有好的设计,甚至没有文档,造成的项目拖延时间是几何级数的;是中国的程序员笨蛋么?更不是,大家的脑子都很聪明!那是 什么呢?我想说的是第一,懒惰;第二,自以为是;第三,意识上还不到真正软件工程师的程度。)

It will be the opportunity to illustrate one of the tasks of the eXtreme Programming (XP) methodology: the writing of stories, and the work that developers have to do to transform stories into functionality. XP is one of the best agile development approaches, and is usually applicable to web 2.0 projects like askeet.

这是个解释极限编程的好例子:stories,开发者需要干的是实现stories为具体功能。入门敏捷开发最好方式之一就是写程序,尤其是类似askeet这样的web 2.0项目。

Stories

Stories

In XP, a story is a brief description of the way an action of the user triggers a reaction of the application. Stories are written by the client of the website (the one who eventually pays for it - the web is not all about open source). Stories rarely exceed one or two sentences. They are regrouped in themes.

在极限编程里,story是用户动作想要实现的功能描述。story由客户提出(最后客户付钱——web并不全是开源免费的)。story很少超过一两句。主要内容写在主题上。

The stories are generally less detailed and more elementary than use cases. If you are familiar with UML, you might find the stories to not be precise enough, but we will see shortly that it can be a great chance.

story通常内容不太详细。如果你熟悉UML,你会发现story不够精确,但是我们将看到这是个巨大的变化。

Stories focus on the result of the action, not the implementation details. Of course, the client may have preferences concerning the interface, and in this case the story has to contain the demands and recommendations about the look and feel of the human computer interaction.

story关注于动作的结果,而不是动作实现过程。当然,客户需要一个界面,story的例子包含了用户体验内容。

Stories have to be small enough to be evaluated easily by developers in terms of development time. Usually, a team of extreme programmers measure stories in units. The value of a unit is refined throughout the course of a project, and can vary from half a day to a few days.

story在开发周期里被开发人员关注的绝对不够。通常,团队里最极端的做法是把处理story的工作放到单元里。单元初始值在项目里被改来改去,一天或者几天都在变化。

Now, let’s have a look at how the client would define the requirements for the askeet backend.

现在,看看askeet后台是怎样定义需求的。

Story #1: Profile management

第一部分:资料管理

Every user can ask to become a moderator. In a user’s profile page, a link should be made available to ask for this privilege. A person who asked to be moderator must not be able to ask it again until he/she receives an answer.

每一个用户都可以申请成为版主。在用户资料页面,显示一个链接到“特权”。而且申请人在收到回复前不能重复申请。

The persons entitled to accept or refuse a moderator candidate are the administrators. They must be able to browse the list of candidates, and have a button to grant or refuse the grade of moderator for each one of them. Administrators need to have a link to the candidate’s profile to see if their contributions are all right.

决定申请是否通过的是管理员。显示一个申请人列表给管理员,每一个名字后面有按钮可以选择通过或者不通过。管理员需要一个链接到申请人的用户资料页,来查看相关信息。

Granting moderator rights must be a reversible action: Administrators must be able to browse a list of moderators, and for each, to delete the moderator credential.

申请通过的动作必须是可逆的:管理员还能列出版主列表,可以取消单个版主权限。

Administrators can also grant administrator rights to other users. They have access to the list of administrators.

管理员也可以赋予其他用户管理员权限。也应该有一个管理员列表。

Story #2: Report of problematic questions or answers

第二部分:报告有争议的问题和答案。

Every user must be able to report a problematic question or answer to a moderator. A simple ‘report spam’ link at the bottom of every question or answer can be a good solution.

每个普通读者都可以报告有争议的问题和答案给版主。一个简单的“report spam”链接显示在问题或者答案下面就实现了这个功能。

To avoid spam of reports, the report from a user about a specific question or answer can only be counted once. It would be great if the user had a visual feedback about the fact that his/her report was taken into account.

为了避免垃圾报告,用户发过来的报告只记数一次。读者在他的账户里收到报错反馈信息,这是个不错的主意。

Story #3: Handling of problematic questions or answers

第三部分:处理有争议的问题和答案

Moderators have two more lists available: the list of problematic questions, and the list of problematic answers. Each list is ordered according to the number of reports, in decreasing order. So the most reported questions will appear on top of the reported question list.

版主可以看到两个或更多个信息列表:问题列表,答案列表。列表内容根据读者提交报告数量降序排列。读者报告最多的问题会显示在列表之首。

Moderators have the ability to delete a question, to delete an answer, and to reset the number of reports about either one. The deletion of a question causes the deletion of all the answers to this question.

版主可以删除问题,删除答案,设置读者提交报告的数量。注意删除问题也就把相关的答案全删除了。

Story #4: handling of problematic tags
第四部分:处理有争议的标签

Moderators have the ability to delete a tag for a question, whether the tag was given by them or not.

版主可以删除有争议的标签,不论这标签是谁提出的。

Moderators have access to a list of tags, ordered by inverse popularity, so that they can detect the problematic tags - the ones that don’t make sense. By linking to the list of questions tagged with this tag, the list gives the ability to suppress the tags.

版主可以访问标签列表,按照人气排序,那么可以删除有争议的标签——那些不确定的。通过标签链接到对应问题列表,禁止这些标签使用。

Story #5: Handling of problematic users

第五部分: 处理有争议的读者

When a moderator deletes a user’s contribution, it increments the number of problematic contributions posted by this user.

当版主删除了读者,就带来了一系列问题。

Administrators have a list of problematic users ordered by the number of problematic posts erased. Administrators must be able to delete a user and all his/her contributions.

管理员删除了读者后,必须删除读者所涉及的所有属性。

Is that all?

这就是全部吗?

Yes, that’s all that the client needs to define about the functionality required for the askeet site management. It doesn’t cover all cases as a functional specification would, it is not as accurate as a complete set of use case, and it leaves a lot of open ends that may lead to unwanted results.

是的,这就是客户要求askeet管理界面所需要实现的功能。可能还不全,但是程序功能大体就这些,现在还有很多情况没有列举出来,这里遗留了很多可能导致问题的情况。

But the job of the agile developers, which starts now, is to detect the possible ambiguities and lack of data, and to require the assistance of the client when it turns out that a story must be more precise. In a XP-style development phase, the client is always available to answer the questions of the development team.

作为一个敏捷开发拥护者,现在需要做的是发现尽可能发现多的模棱两可的问题和缺乏数据,客户提供的需求分析尽可能详细。极限编程周期里,客户最好能给开发队伍提供合适的答案。

So the developers meet up in pairs, and each pair chooses a story to work on. They talk a bit about what the story means, the unit test cases that would validate the functionality. They write the unit tests. Then, they write the code to pass these tests. When it’s done, they release the code that they added in the whole application, and validate the integration by running all the unit tests written before. As it works, they take a cup of coffee, and split up. Then they form a new pair with someone else and focus on a new story.

开发者召开开发会议了,选择一个story来开始写代码。他们讨论一下这个story是什么意思,单元测试检查功能。他们写单元测试程序。他们开发的代码通过了单元测试。把项目代码整合,把以前写过单元测试都跑一遍,可以发布产品了。一切都正常,程序员们去享受咖啡了,放假休息了。然后他们准备接新的工作。

What if the final result doesn’t meet up with the desires of the client? Well, it only represents a few units of work (a few hours or days), so it is easy to forget it and try a new approach. At least, the client now knows what he/she doesn’t want, and that’s a great step towards determinism. But most of the time, as the developers are given the opportunity to talk directly with the client and read between the lines of he stories written, they get to produce the functionality in an even better way than the client would expect. Plus, it’s the developer who knows about the AJAX possibilities and the way a web 2.0 can become successful. So giving them (us) the initiative is a good chance to end up with a great application.

如果最后产品的功能和客户要求不一致怎么办呢?好嘛,只进行了点单元测试(耗费几小时或几天),很容易被遗忘,马上又有新项目开始。最后,客户们知道他们不需要什么了,问题就这样出现了。正确的是做法应该是几乎大部分时间,开发者可以直接和客户讨论并且一起阅读写好的story,他们能提供客户所需要的更好的功能。另外,开发者懂AJAX技术和web 2.0模式。那么让他们发挥自主性,开发出更好的程序。

If you are interested in XP and the benefits of agile development, have a look at the eXtreme Programming website or read Extreme Programming Explained: Embrace Change by Kent Beck.

如果你对极限编程感兴趣而且已经从敏捷开发中尝到了甜头,去敏捷开发的站点看看,理解一下敏捷开发的解释:Embrace Change by Kent Beck。

Backend vs. enhanced frontend

后台 vs. 前台加强版

The feedback of the developer on the client’s requirements is often crucial for the quality of the application. Let us see what the developer, who knows how the application is built and how powerful symfony is, could say to the client.

程序员给客户需求的反馈对程序质量来说是很重要的。看看程序员,那些懂程序如何建立,symfony如何强大的人,是怎样对客户说的。

The idea to add a backend application to askeet is not that good, and for several reasons.

使用后台程序想法不是很好,有以下几点原因。

First, a moderator using the backend might need a lot of the features already available in the frontend (including the list of latest questions, the login module, etc.). So there is a risk that the backend application repeats part of the frontend. As we don’t like to repeat ourselves, that would imply a lot of cross-application refactoring, and this is much too long for the hour dedicated to it. Second, a new application would probably mean a new design to the site, with a custom layout and stylesheets. This is what takes the most time in application development. Last, to create the backend application in one hour, we would probably have to use the CRUD generator a lot, resulting in many unnecessary actions and long-to-adapt templates.

首先,版主用后台需要使用很多前台功能(包括最近问题列表显示,登录等等)。这需要后台重复前台功能。我们讨厌重复代码,这还不包括大量的cross-application修饰代码重装饰,做起来非常费时间。第二,使用新程序代表新设计,要有自定义布局和样式表。开发需要很多时间。最后,在一个小时创建出后台来,我们需要用CRUD生成很多东西,结果大量的、我们并不需要的动作和模板会产生。

In the near future (it is planned for version 0.6), symfony will provide a full-featured back-office generator. All the functionality commonly needed to manage a website activity will be handled easily, almost without a line of code. This brilliant addition would have changed our mind about the way to build the askeet backend, but considering the current state of the framework, the best solution for the management features is to add them to the frontend application.

在后面(现在按照0.6版计划),symfony提供了具有全面特性的后台管理程序生成。所有功能都会被包含,几乎不需要你写一行代码。这个光辉办法将改变我们创建askeet后台的思路,但是考虑到当前框架状态,最好的解决方法是把后台管理特性加到前台程序上。

[译者:看到了没有,askeet的作者果然够懒!]

The base of the askeet frontend is a set of lists, and detail pages for questions and users in which certain actions are available. This is exactly the skeleton needed to build up site management functionality on.

askeet的前台是一套列表,问题和用户详细页的动作都能用。这是创建站点管理功能的脚手架。

Although it would be helpful to show how a project can contain more than one application, the client, impressed by this demonstration, goes for an integration of the site management features in the frontend application.

尽管项目包含更多的程序是很有用的,但是客户会印象很深刻——我们把管理站点的属性加到前台上。

If you are still curious about the way to have more than one application running in a symfony project, have a look at the My first project tutorial, which describes it in detail.

如果你仍然对让多个程序运行在symfony里感兴趣,看看我们第一个项目的教材。

The functionality: what the developers understand

功能:程序员都知道什么

After the developers meet up and talk with the client about the stories, they deduce the modifications to be done to the askeet application. The developer transforms stories to tasks. Tasks are usually smaller than stories, because implementing a story takes more than a day or two, while a task can normally be developed within one or two time units.

在程序员一起讨论了客户的story后,他们推论出程序的修改。程序员把story转换成任务。任务通常比story小,因为说明白story要花费一天或两天时间,用代码实现任务往往很快。

1.The model has to be modified to allow efficient requests:

1.修改模型来让需求更有效率:

new table ReportQuestion to be created, with question_id, user_id and created_at columns

新表ReportQuestion被创建,有question_id,user_id和created_at字段

new table ReportAnswer to be created, with question_id, user_id and created_at columns

新表ReportAnswer被创建,有question_id,user_id和created_at字段

new column reports to be added to the Question and Answer tables
new columns is_administrator, is_moderator and deletions to be added to the User table

新字段reports被加如到问题和答案表,新字段is_moderator和deletions被加到用户表

2.On every page, the sidebar has to provide access to new lists according to the credentials of the user:

2.在每一页,根据用户的身份,侧变栏提供了进入特权操作的入口:

All users: popular questions, latest questions, latest answers

所有用户:问题,最近的问题,最近的答案

Moderators: reported questions, reported answers, unpopular tags

版主:被报告是问题,被报告的答案,有争议的标签

Administrators: administrators, moderators, moderator candidates, problematic users

管理员:管理员,版主,申请版主的人,有争议的用户

3.The question detail page (question/show) has to provide access to new actions according to the credentials of the user:

3.问题详细页(question/show)根据用户身份提供了进入动作的入口:

Subscriber: report question, report answer

发表者:报告的问题,报告的答案

Moderators: delete question and answers, delete answer, reset reports for question, reset reports for answer, delete tag

版主:删除的问题和对应答案,删除的answer,重新设置的问题报告,重新设置答案报告,删除的标签

The question detail has to give additional information according to the credentials of the user:

根据用户身份,问题详细页给出更多信息:

Subscriber: if the question has already been reported by the subscriber

发表者:如果问题已经被其他发表者报告过了

Moderator: the number of reports about the question and answers

版主:问题和答案被报告的数量

4.The user profile page (user/show) has to provide access to new actions according to the credentials of the user:

4.用户资料页(user/show)根据用户身份提供了进入动作的入口:

Subscriber on his own page: come forward as a moderator candidate

发表者的页面:申请版主

Administrators: delete the user and all his/her contributions, grant moderator credentials, refuse moderator credentials, delete moderator credentials, grant administrator credentials

管理员:删除读者和他/她的所有涉及到的属性,通过版主申请,拒绝版主申请,禁用版主权限,通过管理员申请

The user profile page has to give additional information according to the credentials of the user:

用户资料页面根据用户身份给出更多信息:

All users: credentials of the user, credentials being applied for

所有用户:用户身份,帐户状态

Administrators: number of erased posts

管理员:清除内容的数量
5.New lists with restricted access must be created:

5.通到对应列表的限制入口必须被创建:

Restricted to moderators:

版主以上级别访问的:

question/reports: list of reported questions, in decreasing order of number of reports; For each, link to the question detail.

question/report:被报告问题列表,降序排列报告数量;每一个问题都链接到详细页。

answer/reports: list of reported answers, in decreasing order of number of reports; For each, link to the question detail.

answer/report:被报告答案列表,降序排列报告数量;每一个答案都链接到详细页。

tag/unpopular: list of tags, in increasing popularity order; For each, link to the list of questions tagged with this tag

tag/unpopular:标签列表,按人气降序排列;每一个标签都有链接到标签过的问题列表

Restricted to administrators:

管理员以上级别访问的:

user/administrators: list of administrators, by alphabetical order; For each, link to the user profile

user/administrator:管理员列表,按字母排序;每一个都链接到用户资料

user/moderators: list of moderators, by alphabetical order; For each, link to the user profile

user/moderator:版主列表,按字母排序;每一个都链接到用户资料

user/candidates: list of moderator candidates, by alphabetical order; For each, link to the user profile

user/candidate:列出版主申请者,按字母排序;每一个都连接到用户资料

user/problematic: list of problematic users, in decreasing order of deleted contributions; For each, link to the user profile

user/problematic:列出有争议的用户,按照被禁用的属性降序排列;每一个都链接到用户资料

6.Two new credentials must be created: Administrator and Moderator.

6.两种身份必须创建:管理员和版主

7.At least one administrator has to be setup by hand in the database for the application to work.

7.至少要有一个管理员被手动设置在数据库里。

[译者:你是不是快看睡了,看烦了?呵呵,软件文档详细些好,这里多写点,就不容易出问题。]

Implementation

实行

Once the task list is written, the way to implement the backend features on askeet with symfony is just a matter of work. Applying the XP methodology on this task list, including the writing of unit tests, would take at least a good day of work. For the needs of the advent calendar tutorial, we will do it a little faster, and we will just focus here on the new techniques not described previously, or on the ones that should help you to review classical symfony techniques.

一旦开始写程序了,增加后台功能就是一系列工作了。在开发任务表里用极限编程来完成,包括写单元测试程序,一天时间差不多。为了即将完成的教材,我们需要快点做了,我们会专注于新技术使用,而不是像先前那样详细讲解了。

New tables

新数据表

For the question and answer reports, we add two new tables to the askeet database:

给askeet的数据库加两个新表——报告的问题和报告的答案:

<table name="ask_report_question" phpName="ReportQuestion">
<column name="question_id" type="integer" primaryKey="true" />
<foreign-key foreignTable="ask_question">
<reference local="question_id" foreign="id" />
</foreign-key>
<column name="user_id" type="integer" primaryKey="true" />
<foreign-key foreignTable="ask_user">
<reference local="user_id" foreign="id" />
</foreign-key>
<column name="created_at" type="timestamp" />
</table>

<table name=”ask_report_answer” phpName=”ReportAnswer”>
<column name=”answer_id” type=”integer” primaryKey=”true” />
<foreign-key foreignTable=”ask_answer”>
<reference local=”answer_id” foreign=”id” />
</foreign-key>
<column name=”user_id” type=”integer” primaryKey=”true” />
<foreign-key foreignTable=”ask_user”>
<reference local=”user_id” foreign=”id” />
</foreign-key>
<column name=”created_at” type=”timestamp” />
</table>

The combination of the question_id/answer_id and the user id is enough to create a unique primary key, so we don’t need to add an auto-increment id for these tables.

question_id/answer_id和user_id的组合足够创建一个独一无二的主键了,所以我们不需要给这个表加auto-increment了。

We also add a new reports column to the Question and Answer table. In order to synchronize the number of records in the ReportQuestion and the number of reports in the Question table, we override the save() method of the ReportQuestion object to add a transaction, as we did during day 4:

我们也需要把report字段加到问题和答案表里。为了同步报告的问题表和问题表的记录数量,我们重写ReportQuestion对象的save()方法增加一个事物处理,像我们在第四天做的那个:

public function save($con = null)
{
$con = sfContext::getInstance()->getDatabaseConnection('propel');
try
{
$con->begin();

$ret = parent::save();

// update spam_count in answer table
$answer = $this->getAnswer();
$answer->setReports($answer->getReports() + 1);
$answer->save();

$con->commit();

return $ret;
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}

Same for the ReportAnswer table.

对ReportAnswer表一样。

Cascade deletion

级联删除

When a question is deleted, all the answers to this questions must also be deleted, as well as all the interests about the question, the tags added to the question and the relevancy ratings about all the answers. We need a mechanism of cascade deletion to take care of all that for us.

当一个问题被删除,所有对应的答案也必须被删除,对应的“感兴趣”纪录也一样删除,问题的标签和给答案的“顶”和“踩”都一样。我们需要一个级联删除原理来完成。

During day two, we had the idea of using the InnoDB engine for the askeet database. This facilitates the cascade deletions. But the Propel layer can manage to do the cascade deletions even on a non-InnoDB enabled database, provided that we indicate in the schema that cascade deletion has to be taken care of. This has to be done when declaring a foreign key: add a onDelete=”cascade” attribute to the <foreign-key> tag in a table definition. For instance, for the Answer table:

在第二天,我们给askeet数据库用了InnoDB引擎。这个容易使用级联删除。Propel层能控制级联删除,即使数据库没有用InnoDB。我们在schema中注明,级联删除就可以实现。给外键标签加上onDelete=”cascade”属性。例如,对Answer表这样改:

...
<table name="ask_answer" phpName="Answer">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
<column name="question_id" type="integer" />
<foreign-key foreignTable="ask_question" onDelete="cascade">
<reference local="question_id" foreign="id"/>
</foreign-key>
<column name="user_id" type="integer" />
<foreign-key foreignTable="ask_user">
<reference local="user_id" foreign="id"/>
</foreign-key>
<column name="body" type="longvarchar" />
<column name="html_body" type="longvarchar" />
<column name="relevancy_up" type="integer" default="0" />
<column name="relevancy_down" type="integer" default="0" />
<column name="reports" type="integer" default="0" />
<column name="created_at" type="timestamp" />
</table>
...

Once the model is rebuilt, cascade deletion is enabled for the relations bearing the onDelete attribute. When you delete a record in the Question table:

一旦模型被重建,级联删除就可以使用了。当我们删除问题表的一条记录时:

if the database uses the InnoDB engine, the related answers will be deleted automatically by the database itself

如果数据库用了InnoDB引擎,对应的答案会自动被数据库删除

else, the Propel layer will automatically get the related answers, delete them, then delete the question.

否则,propel层会自动得到对应的答案并删除,然后删除问题。

All relations may not involve a cascade deletion. Deleting a user, for instance, should delete his/her interests and ratings for answer relevancies, but not his/her contributions (questions and answers). These contributions should be associated to the anonymous user after deletion.

不是所有的事物都能使用级联删除。删除用户,例如,应该删除他/她给答案发表的“感兴趣”纪录和“顶”“踩”信息,但不要删除他/她发表的内容(问题和答案)。这些发表的东西在删除操作后被转成匿名用户发表的。

So the onDelete attribute has to be set to cascade for the following relations:

所以onDelete属性不得不被设置成如下级联方式:

Answer/QuestionId
Interest/QuestionId
Relevancy/QuestionId
QuestionTag/QuestionId
ReportQuestion/QuestionId
ReportAnswer/AnswerId

Add links in the sidebar for users with credentials

在侧变栏里添加到授权用户的链接

We create a new moderator module to handle all the moderator actions, and an administrator one to handle the administration actions.

我们创建新版主模块来处理所有的版主动作,还有管理员模块来处理管理员动作。

During day seven, we used the component slot technique to store the code of the sidebar in the sidebar module. The links to the new lists will appear there, but they need to be conditionned to a credential. This is simply done by using the $sf_user->hasCredential() method, as seen during day six:

在第七天,我们用组件槽技术来储存侧变栏模块的sidebar代码。到新列表的链接在这里显示,但是需要根据身份区分。用$sf_user->hasCredential()方法简单实现,在第六天看看吧:

// in askeet/apps/frontend/modules/sidebar/templates/_default.php and _question.php:
...
<?php include_partial('sidebar/moderation') ?>

<?php include_partial(’sidebar/administration’) ?>

// in askeet/apps/frontend/modules/sidebar/templates/_moderation.php:
<?php if ($sf_user->hasCredential(’moderator’)): ?>
<h2>moderation</h2>

<ul>
<li><?php echo link_to(’reported questions’, ‘moderator/reportedQuestions’) ?> (<?php echo QuestionPeer::getReportCount() ?>)</li>
<li><?php echo link_to(’reported answers’, ‘moderator/reportedAnswers’) ?> (<?php echo AnswerPeer::getReportCount() ?>)</li>
<li><?php echo link_to(’unpopular tags’, ‘moderator/unpopularTags’) ?></li>
</ul>
<?php endif ?>

// in askeet/apps/frontend/modules/sidebar/templates/_administration.php:

<?php if ($sf_user->hasCredential(’administrator’)): ?>
<h2>administration</h2>

<ul>
<li><?php echo link_to(’moderator candidates’, ‘administrator/moderatorCandidates’) ?> (<?php echo UserPeer::getModeratorCandidatesCount() ?>)</li>
<li><?php echo link_to(’moderator list’, ‘administrator/moderators’) ?></li>
<li><?php echo link_to(’administrator list’, ‘administrator/administrators’) ?></li>
<li><?php echo link_to(’problematic users’, ‘administrator/problematicUsers’) ?> (<?php echo UserPeer::getProblematicUsersCount() ?>)</li>
</ul>
<?php endif ?>

wwwsymfony-projectcom_new-links_moderation_links.gif

The class methods QuestionPeer::getReportCount(), AnswerPeer::getReportCount(), UserPeer::getModeratorCandidatesCount() and UserPeer::getProblematicUsersCount() are to be added to the model. They are all based on the same principle:

QuestionPeer::getReportCount(),AnswerPeer::getReportCount(),UserPeer::getModeratorCandidatesCount()和UserPeer::getProblematicUsersCount()都被加到了模型里。他们都基于同样的原理:

public static function getReportCount()
{
$c = new Criteria();
$c->add(self::REPORTS, 0, Criteria::GREATER_THAN);
$c = self::addPermanentTagToCriteria($c);

return self::doCount($c);
}

AJAX report

AJAX技术实现读者提交报告

We will provide a ‘[report to moderator]’ link to report a question in all the places a question is displayed (in the question lists, in a question detail page). It would be nice if this link was an AJAX one, as in the day eight tutorial. So we add a new helper to the QuestionHelper.php file in the askeet/apps/frontend/lib/helper/ directory:

在所有显示问题的地方(在问题列表里,在问题详细页里),我们提供“[report to moderator]”链接来报告一个问题给版主。如果这个链接是用AJAX实现的,那就更好了,正如第八天的教材。我们给QuestionHelper.php加个helper,在askeet/apps/frontend/lib/helper/下:

function link_to_report_question($question, $user)
{
use_helper('Javascript');

$text = ‘[report to moderator]’;
if ($user->isAuthenticated())
{
$has_already_reported_question = ReportQuestionPeer::retrieveByPk($question->getId(), $user->getSubscriberId());
if ($has_already_reported_question)
{
// already reported for this user
return ‘[reported]’;
}
else
{
return link_to_remote($text, array(
‘url’ => ‘@user_report_question?id=’.$question->getId(),
‘update’ => array(’success’ => ‘report_question_’.$question->getId()),
‘loading’ => “Element.show(’indicator’)”,
‘complete’ => “Element.hide(’indicator’);”.visual_effect(’highlight’, ‘report_question_’.$question->getId()),
));
}
}
else
{
return link_to_login($text);
}
}

Now, the templates where the link has to appear (question/templates/showSuccess.php, question/templates/_list.php) can use this helper:

现在,模板的链接效果(question/templates/showSuccess.php, question/templates/_list.php)使用了这个helper:

<div class="options" id="report_question_<?php echo $question->getId() ?>">
<?php echo link_to_report_question($question, $sf_user) ?>
</div>

The @user_report_question rule has to be written in the routing.yml as leading to a user/reportQuestion action:

@user_report_question规则被写入了routing.yml,通向user/reportQuestion动作:

public function executeReportQuestion()
{
$this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($this->question);

$spam = new ReportQuestion();
$spam->setQuestionId($this->question->getId());
$spam->setUserId($this->getUser()->getSubscriberId());
$spam->save();
}

And the result of this action, the user/templates/reportQuestionSuccess.php template, is simply:

动作传送结果给user/templates/reportQuestionSuccess.php模板:

<?php use_helper('Question') ?>
<?php echo link_to_report_question($question, $sf_user) ?>

wwwsymfony-projectcom_report-question_report_question.gif

The same goes for the reported answers.

同样适用于报告的答案。

New action links for users with credentials

根据用户身份决定到动作的链接

In the question_body div tag of the askeet/apps/frontend/modules/question/templates/showSuccess.php, we add the question management actions for moderators only, so to be compatible with the AJAX report, we put them in a fragment:

在askeet/apps/frontend/modules/question/templates/showSuccess.php中的question_body层标签里,我们增加只供版主用的问题管理动作,为了和AJAX技术兼容,我们写入代码片断:

...
<div class="options" id="report_question_<?php echo $question->getId() ?>">
<?php echo link_to_report_question($question, $sf_user) ?>
<?php include_partial('moderator/question_options', array('question' => $question)) ?>
</div>

The askeet/apps/frontend/modules/moderator/templates/_question_options.php fragment contains:

askeet/apps/frontend/modules/moderator/templates/_question_options.php代码片断包括:

<?php if ($sf_user->hasCredential('moderator')): ?>
<?php if ($question->getReports()): ?>
[<strong><?php echo $question->getReports() ?></strong> reports]
<?php echo link_to('[reset reports]', 'moderator/resetQuestionReports?stripped_title='.$question->getStrippedTitle()) ?>
<?php endif ?>
<?php echo link_to('[delete question]', 'moderator/deleteQuestion?stripped_title='.$question->getStrippedTitle()) ?>
<?php endif ?>
...

wwwsymfony-projectcom_moderator-actions_question_moderator_options.gif

The same options are added in the askeet/apps/frontend/modules/answer/templates/_answer.php, with a link to a moderator/templates/_answer_options.php fragment.

同样的内容被加到了askeet/apps/frontend/modules/answer/templates/_answer.php中,有链接到moderator/templates/_answer_options.php代码片断。

The same kind of adaptation goes for the administrator action links in the user profile page.

同样的改变存在于管理员动作,链接到用户信息页面。

One of the good practices about links to actions is to implement them as a normal link (doing a ‘GET’ request) when the action doesn’t modify the model, and as a button (doing a ‘POST’) request when the action alters the data. This is to avoid that automatic web crawlers, like search engine robots, click on a link that can modify the database. The AJAX links being inmplemented in javascript, they can’y be clicked by robots. The ‘reset’ and ‘report’ links that we just added, however, could be clicked by a robot. Fortunately, they are not displayed unless the user has moderator access, so there is no risk that they are clicked unintentionnally.

连接动作的好办法是当动作没有修改模型时,用普通链接实现(GET方法),当action修改数据时,按纽(POST方法)实现。这是为了防止web爬虫,像搜索引擎的机器人,如果让他点个连接可以修改数据库信息就不好了。AJAX链接用javascript执行,机器人无法点击。reset和report链接(我们刚加的),可以被机器人点击。幸运的是,除非用户是版主,否则不显示,那么就没有点击风险了。

We could add an extra protection on these links by declaring them as ‘POST’ links, as described in the link chapter of the symfony book:

我们可以加个额外保险给链接,声明为POST方式,具体见symfony:

getId(), 'post=true') ?>

Access restriction

限制权限入口

When a user with specific rights logs in, his sfUser object must be given the appropriate credential. This is done in the signIn method of the myUser class in askeet/apps/frontend/lib/myUser.class.php, that we created during day six:

当权限用户登录后,他的sfUser对象必须被赋予适当的身份。在askeet/apps/frontend/lib/myUser.class.php里用myUser类的signIn方法实现,我们第六天的时候创建过:

public function signIn($user)
{
$this->setAttribute('subscriber_id', $user->getId(), 'subscriber');
$this->setAuthenticated(true);

$this->addCredential(’subscriber’);

if ($user->getIsModerator())
{
$this->addCredential(’moderator’);
}

if ($user->getIsAdministrator())
{
$this->addCredential(’administrator’);
}

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

Of course, all the moderator actions have to be restricted to moderators with appropriate settings in the askeet/apps/frontend/modules/moderator/config/security.yml:

在askeet/apps/frontend/modules/moderator/config/security.yml里加些设置,限制版主动作仅被版主访问:

all:
is_secure: on
credentials: moderator

The same kind of restriction is to be applied for administrator actions.

限制的同样类型被应用到了管理员动作。

New moderator and administrator actions

管理员和版主的动作

There is nothing new in the actions to be added to the moderator and administrator actions. We will just give the list here so that you know about them:

加到版主和管理员动作的动作都是空的。具体见下表:

// administrator actions
executeProblematicUsers() -> usersSuccess.php
executeModerators() -> usersSuccess.php
executeAdministrators() -> usersSuccess.php
executeModeratorCandidates() -> usersSuccess.php

executePromoteModerator() -> request referrer
executeRemoveModerator() -> request referrer
executePromoteAdministrator() -> request referrer
executeRemoveAdministrator() -> request referrer

// moderator actions
executeUnpopularTags() -> unpopularTagsSuccess.php
executeReportedQuestions() -> reportedQuestions.php
executeReportedAnswers() -> reportedAnswers.php

executeDeleteTag() -> request referrer
executeDeleteQuestion() -> @homepage
executeDeleteAnswer() -> request referrer

To specify a custom template for an action, you can add a view.yml config file to the module. For instance, to have half of the administrator actions use the usersSuccess.php template, you can create the following askeet/apps/frontend/modules/administrator/config/view.yml file:

指定动作的自定义模板,你可以给模块加个view.yml。例如,一半管理员动作使用userSuccess.php模板,如下创建askeet/apps/frontend/modules/administrator/config/view.yml文件:

moderatorsSuccess:
template: users

administratorsSuccess:
template: users

moderatorCandidatesSuccess:
template: users

problematicUsersSuccess:
template: users

Log deletions

删除的操作日志

When a moderator deletes a question, we want to keep a trace of the deletion in a log file, in a warning message. To allow the logging of warning messages in the production environment, we need to modify the logging.yml configuration file:

当版主删除问题时,我们最好在日志里有所记录。允许在发布环境里使用日志,我们需要修改logging.yml配置文件:

prod:
level: warning

Then, in all the delete actions, add the code to log the deletion, as in this moderator/deleteQuestion action:

接下来,在所有的删除动作里,加上代码实现删除操作记录日志,在moderator/deleteQuestion动作里:

public function executeDeleteQuestion()
{
$question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));
$this->forward404Unless($question);

$con = sfContext::getInstance()->getDatabaseConnection(’propel’);
try
{
$con->begin();

$user = $question->getUser();
$user->setDeletions($user->getDeletions() + 1);
$user->save();

$question->delete();

$con->commit();

// log the deletion
$log = ‘moderator “%s” deleted question “%s”‘;
$log = sprintf($log, $this->getUser()->getNickname(), $question->getTitle());
$this->getContext()->getLogger()->warning($log);
}
catch (PropelException $e)
{
$con->rollback();
throw $e;
}

$this->redirect(‘@homepage’);
}

If you want to know more about logging, you can have a look at the debug chapter of the symfony book.

如果你想知道更多的关于日志的内容,可看看symfony book的debug章节。

We changed the try/catch statement to react only to PropelExceptions instead of all Exceptions. This is because we don’t want the transaction to fail only because there is a problem in the logging of the deletion.

我们改变try/catch程序仅让PropelException起作用。我们不想仅仅因为一个日志问题导致事物处理失败。

In the example above, we use the object $question even after it has been deleted. This is because the call to the ->delete() method marks a record or a list of records for deletion, and the actual deletion is only processed by Propel once the action is finished.

在上面的例子里,我们用$question对象。这是因为->delete()方法标记了删除记录列表或者一个记录,真实的删除是一但动作完成,就用propel实现删除。

Post a Comment