From f92ca63a225d43e7ecca4ebe528c2a1e206a40f1 Mon Sep 17 00:00:00 2001 From: xue <> Date: Tue, 26 Jun 2007 15:59:16 +0000 Subject: Completed day 4 for blog tutorial. --- .../protected/pages/Day3/CreateEditUser.page | 3 +- .../protected/pages/Day3/CreateLoginUser.page | 2 +- .../protected/pages/Day3/CreateNewUser.page | 2 +- .../protected/pages/Day4/CreateEditPost.page | 133 +++++++++++++++ .../protected/pages/Day4/CreateListPost.page | 183 +++++++++++++++++++++ .../protected/pages/Day4/CreateNewPost.page | 135 +++++++++++++++ .../protected/pages/Day4/CreateReadPost.page | 135 +++++++++++++++ .../protected/pages/Day4/Overview.page | 26 +++ .../blog-tutorial/protected/pages/Day4/output.gif | Bin 0 -> 3406 bytes .../blog-tutorial/protected/pages/Day4/output2.gif | Bin 0 -> 6326 bytes .../blog-tutorial/protected/pages/Day4/output3.gif | Bin 0 -> 11874 bytes .../blog-tutorial/protected/pages/Day4/output4.gif | Bin 0 -> 11916 bytes 12 files changed, 615 insertions(+), 4 deletions(-) create mode 100644 demos/blog-tutorial/protected/pages/Day4/CreateEditPost.page create mode 100644 demos/blog-tutorial/protected/pages/Day4/CreateListPost.page create mode 100644 demos/blog-tutorial/protected/pages/Day4/CreateNewPost.page create mode 100644 demos/blog-tutorial/protected/pages/Day4/CreateReadPost.page create mode 100644 demos/blog-tutorial/protected/pages/Day4/Overview.page create mode 100644 demos/blog-tutorial/protected/pages/Day4/output.gif create mode 100644 demos/blog-tutorial/protected/pages/Day4/output2.gif create mode 100644 demos/blog-tutorial/protected/pages/Day4/output3.gif create mode 100644 demos/blog-tutorial/protected/pages/Day4/output4.gif (limited to 'demos/blog-tutorial/protected') diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page index c2e2db86..78c345a3 100644 --- a/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page +++ b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page @@ -156,8 +156,7 @@ class EditUser extends TPage $userRecord->save(); // redirects the browser to the homepage - $url=$this->Service->constructUrl($this->Service->DefaultPage); - $this->Response->redirect($url); + $this->Response->redirect($this->Service->DefaultPageUrl); } } diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page index 6d7967d4..60810807 100644 --- a/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page +++ b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page @@ -92,7 +92,7 @@ class LoginUser extends TPage // obtain the URL of the privileged page that the user wanted to visit originally $url=$this->Application->getModule('auth')->ReturnUrl; if(empty($url)) // the user accesses the login page directly - $url=$this->Service->constructUrl($this->Service->DefaultPage); + $url=$this->Service->DefaultPageUrl; $this->Response->redirect($url); } } diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page index 86da70d1..f0ca94f5 100644 --- a/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page +++ b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page @@ -149,7 +149,7 @@ class NewUser extends TPage $userRecord->save(); // redirects the browser to the homepage - $this->Response->redirect($this->Service->constructUrl($this->Service->DefaultPage)); + $this->Response->redirect($this->Service->DefaultPageUrl); } } } diff --git a/demos/blog-tutorial/protected/pages/Day4/CreateEditPost.page b/demos/blog-tutorial/protected/pages/Day4/CreateEditPost.page new file mode 100644 index 00000000..95d00765 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day4/CreateEditPost.page @@ -0,0 +1,133 @@ + + +

Creating EditPost Page

+ +

+The EditPost page is provided to authors and the administrator to edit existing blog posts. Like the NewPost page, it displays a form to collect the change to the title and content of a post. +

+ +

+We create two files protected/pages/posts/EditPost.page and protected/pages/posts/EditPost.php to save the page template and page class, respectively. +

+ +

Creating Page Template

+

+The EditPost page template is very similar to the NewPost template. Only the page title and the button caption are different. +

+ + +<%@ Title="My Blog - Edit Post" %> + +<com:TContent ID="Main"> + +

Edit Post

+ +Title: +<com:TRequiredFieldValidator + ControlToValidate="TitleEdit" + ErrorMessage="Please provide a title." + Display="Dynamic" /> +
+<com:TTextBox ID="TitleEdit" Columns="50" /> + +
+Content: +<com:TRequiredFieldValidator + ControlToValidate="ContentEdit" + ErrorMessage="Please provide content." + Display="Dynamic" /> +
+<com:THtmlArea ID="ContentEdit" /> + +
+<com:TButton Text="Save" OnClick="saveButtonClicked" /> + +</com:TContent> +
+ + +

Creating Page Class

+ +

+The EditPage page class is slightly complex than NewPage because it needs to load the specified post data first. It also needs to perform additional authorization check. In particular, it needs to ensure that a post can only be editted by the author or the administrator. Such authorization check is not provided by PRADO itself. +

+ + +class EditPost extends TPage +{ + /** + * Initializes the inputs with existing post data. + * This method is invoked by the framework when the page is being initialized. + * @param mixed event parameter + */ + public function onInit($param) + { + parent::onInit($param); + // Retrieves the existing user data. This is equivalent to: + // $postRecord=$this->getPost(); + $postRecord=$this->Post; + // Authorization check: only the author or the administrator can edit the post + if($postRecord->author_id!==$this->User->Name && !$this->User->IsAdmin) + throw new THttpException(500,'You are not allowed to edit this post.'); + + if(!$this->IsPostBack) // if the page is initially requested + { + // Populates the input controls with the existing post data + $this->TitleEdit->Text=$postRecord->title; + $this->ContentEdit->Text=$postRecord->content; + } + } + + /** + * Saves the post if all inputs are valid. + * This method responds to the OnClick event of the "Save" button. + * @param mixed event sender + * @param mixed event parameter + */ + public function saveButtonClicked($sender,$param) + { + if($this->IsValid) // when all validations succeed + { + // Retrieves the existing user data. This is equivalent to: + // $postRecord=$this->getPost(); + $postRecord=$this->Post; + + // Fetches the input data + $postRecord->title=$this->TitleEdit->SafeText; + $postRecord->content=$this->ContentEdit->SafeText; + + // saves to the database via Active Record mechanism + $postRecord->save(); + + // redirects the browser to the ReadPost page + $url=$this->Service->constructUrl('posts.ReadPost',array('id'=>$postRecord->post_id)); + $this->Response->redirect($url); + } + } + + /** + * Returns the post data to be editted. + * @return PostRecord the post data to be editted. + * @throws THttpException if the post data is not found. + */ + protected function getPost() + { + // the ID of the post to be editted is passed via GET parameter 'id' + $postID=(int)$this->Request['id']; + // use Active Record to look for the specified post ID + $postRecord=PostRecord::finder()->findByPk($postID); + if($postRecord===null) + throw new THttpException(500,'Post is not found.'); + return $postRecord; + } +} + + +

Testing

+

+To test the EditPost page, login first and visit the following URL: http://hostname/blog/index.php?page=EditPost&id=1. This URL can also be reached by clicking on the Edit link on a post detail page. +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day4/CreateListPost.page b/demos/blog-tutorial/protected/pages/Day4/CreateListPost.page new file mode 100644 index 00000000..e25c0609 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day4/CreateListPost.page @@ -0,0 +1,183 @@ + + +

Creating ListPost Page

+ +

+The ListPost page shows the latest blog posts in a list. If there are too many posts, they will be displayed in several pages. +

+ +

+Before we proceed with the implementation, we would like to point our homepage to the upcoming ListPage page, because we want the users to see latest posts when they hit the website. To do so, we modify the application configuration protected/application.xml as follows, +

+ + +...... + + + + + + + +

+We now create the template and class files for the ListPost page: protected/pages/posts/ListPost.page and protected/pages/posts/ListPost.php. +

+ +

Creating Page Template

+

+Based on the functionality requirement of the ListPost page, we will use two controls in the page template: +

+ + +

+Below is the content in the page template: +

+ + +<%@ Title="My Blog" %> + +<com:TContent ID="Main"> + +<com:TRepeater ID="Repeater" + ItemRenderer="Application.pages.posts.PostRenderer" + AllowPaging="true" + AllowCustomPaging="true" + PageSize="5" + /> + +<com:TPager ControlToPaginate="Repeater" OnPageIndexChanged="pageChanged" /> + +</com:TContent> + + +

+In the repeater, we specify that the repeated content is to be displayed using the item renderer PostRenderer which we will create soon after. In order for PRADO to find this class, we give the complete namespace path Application.pages.posts.PostRenderer, meaning the class file is protected/pages/posts/PostRenderer.php. +

+ +

+We also set a few other properties of repeater to enable paging. And we set ControlToPaginate property of the pager so that it knows whose repeated content should be paginated. +

+ + +

Creating Page Class

+ +

+From the above page template, we see that we need to write a page class that implements the event handler: pageChanged() (attached to the pager's OnPageIndexChanged event). We also need to populate post data into the repeater according to the current paging setting. The following is the complete source code of the page class: +

+ + +class ListPost extends TPage +{ + /** + * Initializes the repeater. + * This method is invoked by the framework when initializing the page + * @param mixed event parameter + */ + public function onInit($param) + { + parent::onInit($param); + if(!$this->IsPostBack) // if the page is requested the first time + { + // get the total number of posts available + $this->Repeater->VirtualItemCount=PostRecord::finder()->count(); + // populates post data into the repeater + $this->populateData(); + } + } + + /** + * Event handler to the OnPageIndexChanged event of the pager. + * This method is invoked when the user clicks on a page button + * and thus changes the page of posts to display. + */ + public function pageChanged($sender,$param) + { + // change the current page index to the new one + $this->Repeater->CurrentPageIndex=$param->NewPageIndex; + // re-populate data into the repeater + $this->populateData(); + } + + /** + * Determines which page of posts to be displayed and + * populates the repeater with the fetched data. + */ + protected function populateData() + { + $offset=$this->Repeater->CurrentPageIndex*$this->Repeater->PageSize; + $limit=$this->Repeater->PageSize; + if($offset+$limit>$this->Repeater->VirtualItemCount) + $limit=$this->Repeater->VirtualItemCount-$offset; + $this->Repeater->DataSource=$this->getPosts($offset,$limit); + $this->Repeater->dataBind(); + } + + /** + * Fetches posts from database with offset and limit. + */ + protected function getPosts($offset, $limit) + { + // Construts a query criteria + $criteria=new TActiveRecordCriteria; + $criteria->OrdersBy['create_time']='desc'; + $criteria->Limit=$limit; + $criteria->Offset=$offset; + // query for the posts with the above criteria and with author information + return PostRecord::finder()->withAuthor()->findAll($criteria); + } +} + + +

Creating PostRenderer

+ +

+We still need to create the item renderer class PostRenderer. It defines how each post should be displayed in the repeater. We create it as a template control which allows to specify the post presentation using our flexible template syntax. The template and the class files are saved as PostRenderer.tpl and PostRenderer.php files under the protected/pages/posts directory, respectively. +

+ +

Creating Renderer Template

+

+The renderer template specifies the presentation of various fields in a post, including title, author name, post time and content. We link the post title to the ReadPost which shows more details of the selected post. +

+

+The expression $this->Data refers to the data item passed to the repeater. In our case, it is a PostRecord object. Notice how we retrieve the author name of a post by $this->Data->author->username. +

+ + +

+<com:THyperLink Text="<%# $this->Data->title %>" + NavigateUrl="<%# $this->Service->constructUrl('posts.ReadPost',array('id'=>$this->Data->post_id)) %>" /> +

+ +

+Author: +<com:TLiteral Text="<%# $this->Data->author->username %>" />
+Time: +<com:TLiteral Text="<%# date('m/d/Y h:m:sa', $this->Data->create_time) %>" /> +

+ +

+<com:TLiteral Text="<%# $this->Data->content %>" /> +

+
+ +

Creating Renderer Class

+

+The renderer class is very simple. It extends from TRepeaterItemRenderer and contains no other code. +

+ +class PostRenderer extends TRepeaterItemRenderer +{ +} + + +

Testing

+

+To test the ListPost page, visit the URL http://hostname/blog/index.php (remember we have set ListPost as our new homepage). We shall expect to see the following result. Since we only have one post at the moment, the pager will not show up. Later when we finish NewPost, we can add more posts and come back to test the paging again. +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day4/CreateNewPost.page b/demos/blog-tutorial/protected/pages/Day4/CreateNewPost.page new file mode 100644 index 00000000..5011c346 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day4/CreateNewPost.page @@ -0,0 +1,135 @@ + + +

Creating NewPost Page

+ +

+The NewPost page is provided to authenticated users for creating new blog posts. It needs to display a form that collects the information about the new post, including the post title and the post body content. +

+ +

+Because NewPost can only be accessed by authenticated users, we add a page configuration file config.xml under the directory protected/pages/posts. The configuration specifies that guest users cannot access NewPost and EditPost which is to be introduced in the next section. +

+ + + + + + + + + + +

+As the number of our pages expands, we would like to modify MainLayout so that in the footer of our blog pages there are links to various pages, including the homepage, the NewUser page (visible to the administrator only), and the upcoming NewPost page (visible to authenticated users only). +

+ + + + + +

+We now create two files protected/pages/posts/NewPost.page and protected/pages/posts/NewPost.php to save the page template and page class, respectively. +

+ +

Creating Page Template

+

+The NewPost page template contains a TTextBox to collect the post title and a THtmlArea to collect the post content. The latter is a WYSIWYG HTML editor. To ensure the user input is valid, we associate validators with these input controls. +

+ + +<%@ Title="My Blog - New Post" %> + +<com:TContent ID="Main"> + +

Create New Post

+ +Title: +<com:TRequiredFieldValidator + ControlToValidate="TitleEdit" + ErrorMessage="Please provide a title." + Display="Dynamic" /> +
+<com:TTextBox ID="TitleEdit" Columns="50" /> + +
+Content: +<com:TRequiredFieldValidator + ControlToValidate="ContentEdit" + ErrorMessage="Please provide content." + Display="Dynamic" /> +
+<com:THtmlArea ID="ContentEdit" /> + +
+<com:TButton Text="Create" OnClick="createButtonClicked" /> + +</com:TContent> +
+ + +

Creating Page Class

+ +

+From the above page template, we see that we mainly need to write a page class that implements the event handler: createButtonClicked() (attached to the Create button's OnClick event). +

+ + +class NewPost extends TPage +{ + /** + * Creates a new post if all inputs are valid. + * This method responds to the OnClick event of the "create" button. + * @param mixed event sender + * @param mixed event parameter + */ + public function createButtonClicked($sender,$param) + { + if($this->IsValid) // when all validations succeed + { + // populates a PostRecord object with user inputs + $postRecord=new PostRecord; + // using SafeText instead of Text avoids Cross Site Scripting attack + $postRecord->title=$this->TitleEdit->SafeText; + $postRecord->content=$this->ContentEdit->SafeText; + $postRecord->author_id=$this->User->Name; + $postRecord->create_time=time(); + $postRecord->status=0; + + // saves to the database via Active Record mechanism + $postRecord->save(); + + // redirects the browser to the newly created post page + $url=$this->Service->constructUrl('posts.ReadPost',array('id'=>$postRecord->post_id)); + $this->Response->redirect($url); + } + } +} + + +

Testing

+

+To test the NewPost page, login first and click on the New Post link button in the footer of the homepage. Our browser will display the following result with the URL http://hostname/blog/index.php?page=NewPost. +

+ + +When you visit the NewPost page for the first time, you may notice that it takes several seconds for the page to be displayed. This is because PRADO needs to unpack and publish the javascript code and images for the THtmlArea control used in the page. This is done once and for all. + + + +To test the pagination feature that we developed for the ListPost page, we can create five or more posts and see what happens to the homepage. The pager in ListPost displays five posts each page. + + + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day4/CreateReadPost.page b/demos/blog-tutorial/protected/pages/Day4/CreateReadPost.page new file mode 100644 index 00000000..d9b66d33 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day4/CreateReadPost.page @@ -0,0 +1,135 @@ + + +

Creating ReadPost Page

+ +

+The ReadPost page shows the detailed content of a blog post. For authorized users, it also shows link buttons that would allow them to edit or delete the post. +

+ +

+We create two files protected/pages/posts/ReadPost.page and protected/pages/posts/ReadPost.php to save the page template and page class, respectively. +

+ +

Creating Page Template

+

+The ReadPost page template is very similar to the PostRenderer template, both presenting the content of a post. The difference is that ReadPost needs to display two link buttons when the current user is authorized to edit or delete the post. +

+ + +<com:TContent ID="Main"> + +

+<com:TLiteral Text="<%= $this->Post->title %>" /> +

+ +<com:TControl Visible="<%= $this->canEdit() %>"> + Edit | + <com:TLinkButton Text="Delete" + OnClick="deletePost" + Attributes.onclick="javascript:if(!confirm('Are you sure?')) return false;" /> +</com:TControl> + +

+Author: +<com:TLiteral Text="<%= $this->Post->author->username %>" />
+Time: +<com:TLiteral Text="<%= date('m/d/Y h:m:sa', $this->Post->create_time) %>" /> +

+ +

+<com:TLiteral Text="<%= $this->Post->content %>" /> +

+ +</com:TContent> +
+ +

+Many PHP expressions are used in the above template. The expression $this->Post refers to a property defined in the ReadPost page class. It represents the PostRecord object corresponding to the post currently being viewed. +

+ + +Although we use expressions widely in templates, we do not overuse them. A major guideline in determining whether we should use an expression in a template is that the expression should be a property or a simple presentational transformation of the property. By following this guideline, we ensure content and presentation are well separated without losing sufficient flexibility. + + +

+We also notice in the above template that two link buttons are enclosed within a TControl whose visibility is determined by the expression $this->canEdit(). For the Delete link button, we use javascript confirmation dialog to obtain user's confirmation when he clicks to delete the post. +

+ + +All PRADO controls have a very useful property named Attributes which can accept arbitrary name-value pairs. Most PRADO controls will render the name-value pairs in Attributes literally in the corresponding HTML tag. For example, in the Delete link button above, we define an onclick which is rendered as the onclick attribute in the resulting <a> tag. + + + +

Creating Page Class

+ +

+From the above page template, we see that we need to write a page class that implements the event handler: deletePost() (attached to the Delete button's OnClick event). We also need to retrieve the post data specified by the post ID passed via the id GET parameter.

+ + +We implement the post deletion feature in the ReadPost page because it is so natural to do so here. When the user clicks on the Delete button, a javascript confirmation dialog will pop up. If the user confirms it, the deletion will be carried in response to the OnClick event of the Delete button. + + + +class ReadPost extends TPage +{ + private $_post; + /** + * Fetches the post data. + * This method is invoked by the framework when initializing the page + * @param mixed event parameter + */ + public function onInit($param) + { + parent::onInit($param); + // post id is passed via the 'id' GET parameter + $postID=(int)$this->Request['id']; + // retrieves PostRecord with author information filled in + $this->_post=PostRecord::finder()->withAuthor()->findByPk($postID); + if($this->_post===null) // if post id is invalid + throw new THttpException(500,'Unable to find the specified post.'); + // set the page title as the post title + $this->Title=$this->_post->title; + } + + /** + * @return PostRecord the PostRecord currently being viewed + */ + public function getPost() + { + return $this->_post; + } + + /** + * Deletes the post currently being viewed + * This method is invoked when the user clicks on the "Delete" button + */ + public function deletePost($sender,$param) + { + // only the author or the administrator can delete a post + if(!$this->canEdit()) + throw new THttpException('You are not allowed to perform this action.'); + // delete it from DB + $this->_post->delete(); + // redirect the browser to the homepage + $this->Response->redirect($this->Service->DefaultPageUrl); + } + + /** + * @return boolean whether the current user can edit/delete the post being viewed + */ + public function canEdit() + { + // only the author or the administrator can edit/delete a post + return $this->User->Name===$this->Post->author_id || $this->User->IsAdmin; + } +} + + +

Testing

+

+To test the ReadPost page, visit the URL http://hostname/blog/index.php and click on the title of the only post. Our browser will display the following result with the URL http://hostname/blog/index.php?page=ReadPost&id=1. Note, if we do not login, the two link buttons will be invisible. +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day4/Overview.page b/demos/blog-tutorial/protected/pages/Day4/Overview.page new file mode 100644 index 00000000..cbff07c5 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day4/Overview.page @@ -0,0 +1,26 @@ + + +

Post Management Overview

+ +

+In this section, we create pages that are related with post management. In particular, we implement the CRUD (Create-Retrieve-Update-Delete) operations with respect to blog posts. +

+ +

+According to the requirements, we need to create the following pages organized under a new directory protected/pages/posts. +

+ + + +

+After finishing this section, we shall expect to see the following directories and files: +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day4/output.gif b/demos/blog-tutorial/protected/pages/Day4/output.gif new file mode 100644 index 00000000..8c1caea8 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day4/output.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day4/output2.gif b/demos/blog-tutorial/protected/pages/Day4/output2.gif new file mode 100644 index 00000000..7078e6c6 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day4/output2.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day4/output3.gif b/demos/blog-tutorial/protected/pages/Day4/output3.gif new file mode 100644 index 00000000..ff1834a4 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day4/output3.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day4/output4.gif b/demos/blog-tutorial/protected/pages/Day4/output4.gif new file mode 100644 index 00000000..b1208a0d Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day4/output4.gif differ -- cgit v1.2.3