summaryrefslogtreecommitdiff
path: root/demos/blog-tutorial
diff options
context:
space:
mode:
authorxue <>2007-06-26 15:59:16 +0000
committerxue <>2007-06-26 15:59:16 +0000
commitf92ca63a225d43e7ecca4ebe528c2a1e206a40f1 (patch)
tree0fb92aafab5fedc866c6cafd49433d72bf719114 /demos/blog-tutorial
parentc31e5ab3ebd4ae1561cbdaeaa504d1095dcbbb0a (diff)
Completed day 4 for blog tutorial.
Diffstat (limited to 'demos/blog-tutorial')
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page3
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page2
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page2
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/CreateEditPost.page133
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/CreateListPost.page183
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/CreateNewPost.page135
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/CreateReadPost.page135
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/Overview.page26
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/output.gifbin0 -> 3406 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/output2.gifbin0 -> 6326 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/output3.gifbin0 -> 11874 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day4/output4.gifbin0 -> 11916 bytes
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php2
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php2
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php2
-rw-r--r--demos/blog-tutorial/samples/day4/blog/index.php22
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/.htaccess1
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/BlogUser.php59
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/application.xml56
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/data/blog.dbbin0 -> 4096 bytes
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/database/PostRecord.php28
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/database/UserRecord.php28
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.php19
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.tpl40
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.page47
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.php30
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/Home.page7
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.page27
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.php72
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.page14
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.php64
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.page27
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.php34
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.php7
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.tpl15
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.page25
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.php57
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/posts/config.xml6
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.page40
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.php36
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.page61
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.php83
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.page28
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.php37
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.page73
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.php45
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/pages/users/config.xml7
-rw-r--r--demos/blog-tutorial/samples/day4/blog/protected/schema.sql25
48 files changed, 1738 insertions, 7 deletions
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 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>EditPost</tt> Page</h1>
+
+<p>
+The <tt>EditPost</tt> page is provided to authors and the administrator to edit existing blog posts. Like the <a href="?page=Day4.CreateNewPost">NewPost</a> page, it displays a form to collect the change to the title and content of a post.
+</p>
+
+<p>
+We create two files <tt>protected/pages/posts/EditPost.page</tt> and <tt>protected/pages/posts/EditPost.php</tt> to save the page template and page class, respectively.
+</p>
+
+<h2>Creating Page Template</h2>
+<p>
+The <tt>EditPost</tt> page template is very similar to the <tt>NewPost</tt> template. Only the page title and the button caption are different.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - Edit Post" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Edit Post</h1>
+
+<span>Title:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="TitleEdit"
+ ErrorMessage="Please provide a title."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="TitleEdit" Columns="50" />
+
+<br/>
+<span>Content:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="ContentEdit"
+ ErrorMessage="Please provide content."
+ Display="Dynamic" />
+<br/>
+&lt;com:THtmlArea ID="ContentEdit" />
+
+<br/>
+&lt;com:TButton Text="Save" OnClick="saveButtonClicked" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+The <tt>EditPage</tt> page class is slightly complex than <tt>NewPage</tt> 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.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="php">
+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;
+ }
+}
+</com:TTextHighlighter>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>EditPost</tt> page, login first and visit the following URL: <tt>http://hostname/blog/index.php?page=EditPost&id=1</tt>. This URL can also be reached by clicking on the <tt>Edit</tt> link on a post detail page.
+</p>
+
+<img src="<%~ output4.gif %>" class="output" />
+
+</com:TContent> \ 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 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>ListPost</tt> Page</h1>
+
+<p>
+The <tt>ListPost</tt> page shows the latest blog posts in a list. If there are too many posts, they will be displayed in several pages.
+</p>
+
+<p>
+Before we proceed with the implementation, we would like to point our homepage to the upcoming <tt>ListPage</tt> page, because we want the users to see latest posts when they hit the website. To do so, we modify the application configuration <tt>protected/application.xml</tt> as follows,
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="xml">
+......
+<services>
+ <service id="page" class="TPageService" DefaultPage="posts.ListPost">
+ <pages MasterClass="Application.layouts.MainLayout" />
+ </service>
+</services>
+</com:TTextHighlighter>
+
+<p>
+We now create the template and class files for the <tt>ListPost</tt> page: <tt>protected/pages/posts/ListPost.page</tt> and <tt>protected/pages/posts/ListPost.php</tt>.
+</p>
+
+<h2>Creating Page Template</h2>
+<p>
+Based on the functionality requirement of the <tt>ListPost</tt> page, we will use two controls in the page template:
+</p>
+<ul>
+<li><a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Repeater">TRepeater</a>: this control is mainly used to display a list of data items. The presentation of the each data item can be specified via an inline template or an external template control (the approach we will use here).</li>
+<li><a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Pager">TPager</a>: this control is used to paginate a list of data items. It interacts with end-users to determine which page of data to be displayed in a <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.List">list control</a> (e.g. <tt>TListBox</tt>) or <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Data">data control</a> (e.g. <tt>TRepeater</tt>).</li>
+</ul>
+
+<p>
+Below is the content in the page template:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog" %>
+
+&lt;com:TContent ID="Main">
+
+&lt;com:TRepeater ID="Repeater"
+ ItemRenderer="Application.pages.posts.PostRenderer"
+ AllowPaging="true"
+ AllowCustomPaging="true"
+ PageSize="5"
+ />
+
+&lt;com:TPager ControlToPaginate="Repeater" OnPageIndexChanged="pageChanged" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+<p>
+In the repeater, we specify that the repeated content is to be displayed using the item renderer <tt>PostRenderer</tt> which we will create soon after. In order for PRADO to find this class, we give the complete namespace path <tt>Application.pages.posts.PostRenderer</tt>, meaning the class file is <tt>protected/pages/posts/PostRenderer.php</tt>.
+</p>
+
+<p>
+We also set a few other properties of repeater to enable paging. And we set <tt>ControlToPaginate</tt> property of the pager so that it knows whose repeated content should be paginated.
+</p>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+From the above page template, we see that we need to write a page class that implements the event handler: <tt>pageChanged()</tt> (attached to the pager's <tt>OnPageIndexChanged</tt> 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:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="php">
+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);
+ }
+}
+</com:TTextHighlighter>
+
+<h2>Creating <tt>PostRenderer</tt></h2>
+
+<p>
+We still need to create the item renderer class <tt>PostRenderer</tt>. 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 <tt>PostRenderer.tpl</tt> and <tt>PostRenderer.php</tt> files under the <tt>protected/pages/posts</tt> directory, respectively.
+</p>
+
+<h3>Creating Renderer Template</h3>
+<p>
+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 <tt>ReadPost</tt> which shows more details of the selected post.
+</p>
+<p>
+The expression <tt>$this->Data</tt> refers to the data item passed to the repeater. In our case, it is a <tt>PostRecord</tt> object. Notice how we retrieve the author name of a post by <tt>$this->Data->author->username</tt>.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+<h3>
+&lt;com:THyperLink Text="&lt;%# $this->Data->title %>"
+ NavigateUrl="&lt;%# $this->Service->constructUrl('posts.ReadPost',array('id'=>$this->Data->post_id)) %>" />
+</h3>
+
+<p>
+Author:
+&lt;com:TLiteral Text="&lt;%# $this->Data->author->username %>" /><br/>
+Time:
+&lt;com:TLiteral Text="&lt;%# date('m/d/Y h:m:sa', $this->Data->create_time) %>" />
+</p>
+
+<p>
+&lt;com:TLiteral Text="&lt;%# $this->Data->content %>" />
+</p>
+</com:TTextHighlighter>
+
+<h3>Creating Renderer Class</h3>
+<p>
+The renderer class is very simple. It extends from <tt>TRepeaterItemRenderer</tt> and contains no other code.
+</p>
+<com:TTextHighlighter CssClass="source" Language="php">
+class PostRenderer extends TRepeaterItemRenderer
+{
+}
+</com:TTextHighlighter>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>ListPost</tt> page, visit the URL <tt>http://hostname/blog/index.php</tt> (remember we have set <tt>ListPost</tt> 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 <tt>NewPost</tt>, we can add more posts and come back to test the paging again.
+</p>
+
+<img src="<%~ output.gif %>" class="output" />
+
+</com:TContent> \ 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 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>NewPost</tt> Page</h1>
+
+<p>
+The <tt>NewPost</tt> 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.
+</p>
+
+<p>
+Because <tt>NewPost</tt> can only be accessed by authenticated users, we add a page configuration file <tt>config.xml</tt> under the directory <tt>protected/pages/posts</tt>. The configuration specifies that guest users cannot access <tt>NewPost</tt> and <tt>EditPost</tt> which is to be introduced in the next section.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="xml">
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <authorization>
+ <deny pages="NewPost,EditPost" roles="?" />
+ </authorization>
+</configuration>
+</com:TTextHighlighter>
+
+<p>
+As the number of our pages expands, we would like to modify <tt>MainLayout</tt> so that in the footer of our blog pages there are links to various pages, including the homepage, the <a href="?page=Day3.CreateNewUser">NewUser</a> page (visible to the administrator only), and the upcoming <tt>NewPost</tt> page (visible to authenticated users only).
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+<div id="footer">
+<a href="&lt;%= $this->Service->DefaultPageUrl %>">Home</a>
+
+&lt;com:THyperLink Text="New Post"
+ NavigateUrl="&lt;%= $this->Service->constructUrl('posts.NewPost') %>"
+ Visible="&lt;%= !$this->User->IsGuest %>" />
+
+&lt;com:THyperLink Text="New User"
+ NavigateUrl="&lt;%= $this->Service->constructUrl('users.NewUser') %>"
+ Visible="&lt;%= $this->User->IsAdmin %>" />
+...other links...
+</div>
+</com:TTextHighlighter>
+
+<p>
+We now create two files <tt>protected/pages/posts/NewPost.page</tt> and <tt>protected/pages/posts/NewPost.php</tt> to save the page template and page class, respectively.
+</p>
+
+<h2>Creating Page Template</h2>
+<p>
+The <tt>NewPost</tt> page template contains a <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.TextBox">TTextBox</a> to collect the post title and a <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.HtmlArea">THtmlArea</a> 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.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - New Post" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Create New Post</h1>
+
+<span>Title:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="TitleEdit"
+ ErrorMessage="Please provide a title."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="TitleEdit" Columns="50" />
+
+<br/>
+<span>Content:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="ContentEdit"
+ ErrorMessage="Please provide content."
+ Display="Dynamic" />
+<br/>
+&lt;com:THtmlArea ID="ContentEdit" />
+
+<br/>
+&lt;com:TButton Text="Create" OnClick="createButtonClicked" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+From the above page template, we see that we mainly need to write a page class that implements the event handler: <tt>createButtonClicked()</tt> (attached to the <tt>Create</tt> button's <tt>OnClick</tt> event).
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="php">
+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);
+ }
+ }
+}
+</com:TTextHighlighter>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>NewPost</tt> page, login first and click on the <tt>New Post</tt> link button in the footer of the homepage. Our browser will display the following result with the URL <tt>http://hostname/blog/index.php?page=NewPost</tt>.
+</p>
+
+<com:InfoBox>
+When you visit the <tt>NewPost</tt> 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 <tt>THtmlArea</tt> control used in the page. This is done once and for all.
+</com:InfoBox>
+
+<com:TipBox>
+To test the pagination feature that we developed for the <a href="?page=Day4.CreateListPost">ListPost</a> page, we can create five or more posts and see what happens to the homepage. The pager in <tt>ListPost</tt> displays five posts each page.
+</com:TipBox>
+
+<img src="<%~ output3.gif %>" class="output" />
+
+</com:TContent> \ 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 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>ReadPost</tt> Page</h1>
+
+<p>
+The <tt>ReadPost</tt> 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.
+</p>
+
+<p>
+We create two files <tt>protected/pages/posts/ReadPost.page</tt> and <tt>protected/pages/posts/ReadPost.php</tt> to save the page template and page class, respectively.
+</p>
+
+<h2>Creating Page Template</h2>
+<p>
+The <tt>ReadPost</tt> page template is very similar to the <tt>PostRenderer</tt> template, both presenting the content of a post. The difference is that <tt>ReadPost</tt> needs to display two link buttons when the current user is authorized to edit or delete the post.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;com:TContent ID="Main">
+
+<h2>
+&lt;com:TLiteral Text="&lt;%= $this->Post->title %>" />
+</h2>
+
+&lt;com:TControl Visible="&lt;%= $this->canEdit() %>">
+ <a href="&lt;%= $this->Service->constructUrl('EditPost',array('id'=>$this->Post->post_id))%>">Edit</a> |
+ &lt;com:TLinkButton Text="Delete"
+ OnClick="deletePost"
+ Attributes.onclick="javascript:if(!confirm('Are you sure?')) return false;" />
+&lt;/com:TControl>
+
+<p>
+Author:
+&lt;com:TLiteral Text="&lt;%= $this->Post->author->username %>" /><br/>
+Time:
+&lt;com:TLiteral Text="&lt;%= date('m/d/Y h:m:sa', $this->Post->create_time) %>" />
+</p>
+
+<p>
+&lt;com:TLiteral Text="&lt;%= $this->Post->content %>" />
+</p>
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+<p>
+Many PHP expressions are used in the above template. The expression <tt>$this->Post</tt> refers to a property defined in the <tt>ReadPost</tt> page class. It represents the <tt>PostRecord</tt> object corresponding to the post currently being viewed.
+</p>
+
+<com:InfoBox>
+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 <i>the expression should be a property or a simple presentational transformation of the property</i>. By following this guideline, we ensure content and presentation are well separated without losing sufficient flexibility.
+</com:InfoBox>
+
+<p>
+We also notice in the above template that two link buttons are enclosed within a <tt>TControl</tt> whose visibility is determined by the expression <tt>$this->canEdit()</tt>. For the <tt>Delete</tt> link button, we use javascript confirmation dialog to obtain user's confirmation when he clicks to delete the post.
+</p>
+
+<com:InfoBox>
+All PRADO controls have a very useful property named <tt>Attributes</tt> which can accept arbitrary name-value pairs. Most PRADO controls will render the name-value pairs in <tt>Attributes</tt> literally in the corresponding HTML tag. For example, in the <tt>Delete</tt> link button above, we define an <tt>onclick</tt> which is rendered as the <tt>onclick</tt> attribute in the resulting <tt>&lt;a&gt;</tt> tag.
+</com:InfoBox>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+From the above page template, we see that we need to write a page class that implements the event handler: <tt>deletePost()</tt> (attached to the <tt>Delete</tt> button's <tt>OnClick</tt> event). We also need to retrieve the post data specified by the post ID passed via the <tt>id</tt> GET parameter. </p>
+
+<com:InfoBox>
+We implement the post deletion feature in the <tt>ReadPost</tt> page because it is so natural to do so here. When the user clicks on the <tt>Delete</tt> button, a javascript confirmation dialog will pop up. If the user confirms it, the deletion will be carried in response to the <tt>OnClick</tt> event of the <tt>Delete</tt> button.
+</com:InfoBox>
+
+<com:TTextHighlighter CssClass="source" Language="php">
+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;
+ }
+}
+</com:TTextHighlighter>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>ReadPost</tt> page, visit the URL <tt>http://hostname/blog/index.php</tt> and click on the title of the only post. Our browser will display the following result with the URL <tt>http://hostname/blog/index.php?page=ReadPost&id=1</tt>. Note, if we do not login, the two link buttons will be invisible.
+</p>
+
+<img src="<%~ output2.gif %>" class="output" />
+
+</com:TContent> \ 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 @@
+<com:TContent ID="Main">
+
+<h1>Post Management Overview</h1>
+
+<p>
+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.
+</p>
+
+<p>
+According to the requirements, we need to create the following pages organized under a new directory <tt>protected/pages/posts</tt>.
+</p>
+
+<ul>
+ <li><tt>ListPost</tt> displays posts by their creation time in descending order.</li>
+ <li><tt>ReadPost</tt> displays the detail of a post.</li>
+ <li><tt>NewPost</tt> allows registered users to create a new post.</li>
+ <li><tt>EditPost</tt> allows the author or the administrator to edit an existing post.</li>
+</ul>
+
+<p>
+After finishing this section, we shall expect to see the following directories and files:
+</p>
+
+<img src="%~ directories.gif %>" class="output" />
+
+</com:TContent> \ 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
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day4/output.gif
Binary files 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
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day4/output2.gif
Binary files 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
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day4/output3.gif
Binary files 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
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day4/output4.gif
Binary files differ
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php
index cb69b9d5..81538c33 100644
--- a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php
+++ b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php
@@ -54,7 +54,7 @@ class EditUser 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/samples/day3/blog/protected/pages/users/LoginUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php
index dff8f2f0..a0955490 100644
--- a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php
+++ b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php
@@ -28,7 +28,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/samples/day3/blog/protected/pages/users/NewUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php
index 005fb7d2..76e8cb88 100644
--- a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php
+++ b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php
@@ -37,7 +37,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/samples/day4/blog/index.php b/demos/blog-tutorial/samples/day4/blog/index.php
new file mode 100644
index 00000000..7ccffe49
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/index.php
@@ -0,0 +1,22 @@
+<?php
+
+// The following directory checks may be removed if performance is required
+$basePath=dirname(__FILE__);
+$frameworkPath=$basePath.'/../../../../../framework/prado.php';
+$assetsPath=$basePath.'/assets';
+$runtimePath=$basePath.'/protected/runtime';
+
+if(!is_file($frameworkPath))
+ die("Unable to find prado framework path $frameworkPath.");
+if(!is_writable($assetsPath))
+ die("Please make sure that the directory $assetsPath is writable by Web server process.");
+if(!is_writable($runtimePath))
+ die("Please make sure that the directory $runtimePath is writable by Web server process.");
+
+
+require_once($frameworkPath);
+
+$application=new TApplication;
+$application->run();
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/.htaccess b/demos/blog-tutorial/samples/day4/blog/protected/.htaccess
new file mode 100644
index 00000000..3418e55a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/.htaccess
@@ -0,0 +1 @@
+deny from all \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/BlogUser.php b/demos/blog-tutorial/samples/day4/blog/protected/BlogUser.php
new file mode 100644
index 00000000..6b9e0a23
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/BlogUser.php
@@ -0,0 +1,59 @@
+<?php
+
+// Include TDbUserManager.php file which defines TDbUser
+Prado::using('System.Security.TDbUserManager');
+
+/**
+ * BlogUser Class.
+ * BlogUser represents the user data that needs to be kept in session.
+ * Default implementation keeps username and role information.
+ */
+class BlogUser extends TDbUser
+{
+ /**
+ * Creates a BlogUser object based on the specified username.
+ * This method is required by TDbUser. It checks the database
+ * to see if the specified username is there. If so, a BlogUser
+ * object is created and initialized.
+ * @param string the specified username
+ * @return BlogUser the user object, null if username is invalid.
+ */
+ public function createUser($username)
+ {
+ // use UserRecord Active Record to look for the specified username
+ $userRecord=UserRecord::finder()->findByPk($username);
+ if($userRecord instanceof UserRecord) // if found
+ {
+ $user=new BlogUser($this->Manager);
+ $user->Name=$username; // set username
+ $user->Roles=($userRecord->role==1?'admin':'user'); // set role
+ $user->IsGuest=false; // the user is not a guest
+ return $user;
+ }
+ else
+ return null;
+ }
+
+ /**
+ * Checks if the specified (username, password) is valid.
+ * This method is required by TDbUser.
+ * @param string username
+ * @param string password
+ * @return boolean whether the username and password are valid.
+ */
+ public function validateUser($username,$password)
+ {
+ // use UserRecord Active Record to look for the (username, password) pair.
+ return UserRecord::finder()->findBy_username_AND_password($username,$password)!==null;
+ }
+
+ /**
+ * @return boolean whether this user is an administrator.
+ */
+ public function getIsAdmin()
+ {
+ return $this->isInRole('admin');
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/application.xml b/demos/blog-tutorial/samples/day4/blog/protected/application.xml
new file mode 100644
index 00000000..fd6781df
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/application.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<application id="blog" mode="Debug">
+ <paths>
+ <using namespace="Application.database.*" />
+ <using namespace="Application.common.*" />
+ </paths>
+
+ <!-- configurations for modules -->
+ <modules>
+ <!-- Remove this comment mark to enable caching
+ <module id="cache" class="System.Caching.TDbCache" />
+ -->
+
+ <!-- Remove this comment mark to enable PATH url format
+ <module id="request" class="THttpRequest" UrlFormat="Path" />
+ -->
+
+ <!-- Remove this comment mark to enable logging
+ <module id="log" class="System.Util.TLogRouter">
+ <route class="TBrowserLogRoute" Categories="System" />
+ </module>
+ -->
+ <module id="db" class="System.Data.TDataSourceConfig">
+ <database ConnectionString="sqlite:protected/data/blog.db" />
+ </module>
+
+ <module
+ class="System.Data.ActiveRecord.TActiveRecordConfig"
+ ConnectionID="db" />
+
+ <module id="auth"
+ class="System.Security.TAuthManager"
+ UserManager="users"
+ LoginPage="users.LoginUser" />
+
+ <module id="users"
+ class="System.Security.TDbUserManager"
+ UserClass="Application.BlogUser" />
+
+ </modules>
+
+ <!-- configuration for available services -->
+ <services>
+ <service id="page" class="TPageService" DefaultPage="posts.ListPost">
+ <pages MasterClass="Application.layouts.MainLayout" />
+ </service>
+ </services>
+
+ <!-- application parameters
+ <parameters>
+ <parameter id="param1" value="value1" />
+ <parameter id="param2" value="value2" />
+ </parameters>
+ -->
+</application> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/data/blog.db b/demos/blog-tutorial/samples/day4/blog/protected/data/blog.db
new file mode 100644
index 00000000..fa48526a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/data/blog.db
Binary files differ
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/database/PostRecord.php b/demos/blog-tutorial/samples/day4/blog/protected/database/PostRecord.php
new file mode 100644
index 00000000..01f84437
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/database/PostRecord.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Auto generated by prado-cli.php on 2007-04-07 10:44:20.
+ */
+class PostRecord extends TActiveRecord
+{
+ const TABLE='posts';
+
+ public $post_id;
+ public $author_id;
+ public $create_time;
+ public $title;
+ public $content;
+ public $status;
+
+ public $author;
+
+ protected static $RELATIONS=array
+ (
+ 'author' => array(self::BELONGS_TO, 'UserRecord'),
+ );
+
+ public static function finder($className=__CLASS__)
+ {
+ return parent::finder($className);
+ }
+}
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/database/UserRecord.php b/demos/blog-tutorial/samples/day4/blog/protected/database/UserRecord.php
new file mode 100644
index 00000000..18d5ebbe
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/database/UserRecord.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Auto generated by prado-cli.php on 2007-04-07 10:44:25.
+ */
+class UserRecord extends TActiveRecord
+{
+ const TABLE='users';
+
+ public $username;
+ public $email;
+ public $password;
+ public $role;
+ public $first_name;
+ public $last_name;
+
+ public $posts=array();
+
+ protected static $RELATIONS=array
+ (
+ 'posts' => array(self::HAS_MANY, 'PostRecord'),
+ );
+
+ public static function finder($className=__CLASS__)
+ {
+ return parent::finder($className);
+ }
+}
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.php b/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.php
new file mode 100644
index 00000000..46c1483d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.php
@@ -0,0 +1,19 @@
+<?php
+
+class MainLayout extends TTemplateControl
+{
+ /**
+ * Logs out a user.
+ * This method responds to the "logout" button's OnClick event.
+ * @param mixed event sender
+ * @param mixed event parameter
+ */
+ public function logoutButtonClicked($sender,$param)
+ {
+ $this->Application->getModule('auth')->logout();
+ $url=$this->Service->constructUrl($this->Service->DefaultPage);
+ $this->Response->redirect($url);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.tpl b/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.tpl
new file mode 100644
index 00000000..3f0a9a4f
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/layouts/MainLayout.tpl
@@ -0,0 +1,40 @@
+<html>
+<com:THead />
+<body>
+<com:TForm>
+
+<div id="header">
+<h1>My PRADO Blog</h1>
+</div>
+
+<div id="main">
+<com:TContentPlaceHolder ID="Main" />
+</div>
+
+<div id="footer">
+
+<a href="<%= $this->Service->DefaultPageUrl %>">Home</a>
+
+<com:THyperLink Text="New Post"
+ NavigateUrl="<%= $this->Service->constructUrl('posts.NewPost') %>"
+ Visible="<%= !$this->User->IsGuest %>" />
+
+<com:THyperLink Text="New User"
+ NavigateUrl="<%= $this->Service->constructUrl('users.NewUser') %>"
+ Visible="<%= $this->User->IsAdmin %>" />
+
+<com:THyperLink Text="Login"
+ NavigateUrl="<%= $this->Service->constructUrl('users.LoginUser') %>"
+ Visible="<%= $this->User->IsGuest %>" />
+
+<com:TLinkButton Text="Logout"
+ OnClick="logoutButtonClicked"
+ Visible="<%= !$this->User->IsGuest %>" />
+
+<br/>
+<%= PRADO::poweredByPrado() %>
+</div>
+
+</com:TForm>
+</body>
+</html> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.page
new file mode 100644
index 00000000..c36149ca
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.page
@@ -0,0 +1,47 @@
+<%@ Title="My Blog - Contact" %>
+
+<com:TContent ID="Main">
+
+<h1>Contact</h1>
+<p>Please fill out the following form to let me know your feedback on my blog. Thanks!</p>
+
+<span>Your Name:</span>
+<com:TRequiredFieldValidator ControlToValidate="Name"
+ ErrorMessage="Please provide your name."
+ Display="Dynamic"
+ />
+<br/>
+<com:TTextBox ID="Name" />
+
+<br/>
+
+<span>Your Email:</span>
+<com:TRequiredFieldValidator ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic"
+ />
+<com:TEmailAddressValidator ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic"
+ />
+<br/>
+<com:TTextBox ID="Email" />
+
+<br/>
+
+<span>Feedback:</span>
+<com:TRequiredFieldValidator ControlToValidate="Feedback"
+ ErrorMessage="Please provide your feedback."
+ Display="Dynamic"
+ />
+<br/>
+<com:TTextBox ID="Feedback"
+ TextMode="MultiLine"
+ Rows="10"
+ Columns="40" />
+
+<br/>
+
+<com:TButton Text="Submit" OnClick="submitButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.php
new file mode 100644
index 00000000..b6ce575e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/Contact.php
@@ -0,0 +1,30 @@
+<?php
+
+class Contact extends TPage
+{
+ /**
+ * Event handler for the OnClick event of the submit button.
+ * @param TButton the button triggering the event
+ * @param TEventParameter event parameter (null here)
+ */
+ public function submitButtonClicked($sender, $param)
+ {
+ if ($this->IsValid) // check if input validation is successful
+ {
+ // obtain the user name, email, feedback from the textboxes
+ $name = $this->Name->Text;
+ $email = $this->Email->Text;
+ $feedback = $this->Feedback->Text;
+
+ // send an email to administrator with the above information
+ $this->mailFeedback($name, $email, $feedback);
+ }
+ }
+
+ protected function mailFeedback($name, $email, $feedback)
+ {
+ // implementation of sending the feedback email
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/Home.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/Home.page
new file mode 100644
index 00000000..7a9c4a7d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/Home.page
@@ -0,0 +1,7 @@
+<%@ Title="Welcome to PRADO" %>
+
+<com:TContent ID="Main">
+
+<h1>Welcome to PRADO!</h1>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.page
new file mode 100644
index 00000000..579d833e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.page
@@ -0,0 +1,27 @@
+<%@ Title="My Blog - Edit Post" %>
+
+<com:TContent ID="Main">
+
+<h1>Edit Post</h1>
+
+<span>Title:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="TitleEdit"
+ ErrorMessage="Please provide a title."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="TitleEdit" Columns="50" />
+
+<br/>
+<span>Content:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="ContentEdit"
+ ErrorMessage="Please provide content."
+ Display="Dynamic" />
+<br/>
+<com:THtmlArea ID="ContentEdit" />
+
+<br/>
+<com:TButton Text="Save" OnClick="saveButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.php
new file mode 100644
index 00000000..e137b85e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/EditPost.php
@@ -0,0 +1,72 @@
+<?php
+
+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;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.page
new file mode 100644
index 00000000..e26bc2f5
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.page
@@ -0,0 +1,14 @@
+<%@ 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> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.php
new file mode 100644
index 00000000..7402dace
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ListPost.php
@@ -0,0 +1,64 @@
+<?php
+
+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);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.page
new file mode 100644
index 00000000..bd46dfbb
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.page
@@ -0,0 +1,27 @@
+<%@ Title="My Blog - New Post" %>
+
+<com:TContent ID="Main">
+
+<h1>Create New Post</h1>
+
+<span>Title:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="TitleEdit"
+ ErrorMessage="Please provide a title."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="TitleEdit" Columns="50" />
+
+<br/>
+<span>Content:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="ContentEdit"
+ ErrorMessage="Please provide content."
+ Display="Dynamic" />
+<br/>
+<com:THtmlArea ID="ContentEdit" />
+
+<br/>
+<com:TButton Text="Create" OnClick="createButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.php
new file mode 100644
index 00000000..a5e3ea4d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/NewPost.php
@@ -0,0 +1,34 @@
+<?php
+
+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);
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.php
new file mode 100644
index 00000000..cf0539a1
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.php
@@ -0,0 +1,7 @@
+<?php
+
+class PostRenderer extends TRepeaterItemRenderer
+{
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.tpl b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.tpl
new file mode 100644
index 00000000..862df1ab
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/PostRenderer.tpl
@@ -0,0 +1,15 @@
+<h3>
+<com:THyperLink Text="<%# $this->Data->title %>"
+ NavigateUrl="<%# $this->Service->constructUrl('posts.ReadPost',array('id'=>$this->Data->post_id)) %>" />
+</h3>
+
+<p>
+Author:
+<com:TLiteral Text="<%# $this->Data->author->username %>" /><br/>
+Time:
+<com:TLiteral Text="<%# date('m/d/Y h:m:sa', $this->Data->create_time) %>" />
+</p>
+
+<p>
+<com:TLiteral Text="<%# $this->Data->content %>" />
+</p>
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.page
new file mode 100644
index 00000000..60a8cf1e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.page
@@ -0,0 +1,25 @@
+<com:TContent ID="Main">
+
+<h2>
+<com:TLiteral Text="<%= $this->Post->title %>" />
+</h2>
+
+<com:TControl Visible="<%= $this->canEdit() %>">
+ <a href="<%= $this->Service->constructUrl('posts.EditPost',array('id'=>$this->Post->post_id))%>">Edit</a> |
+ <com:TLinkButton Text="Delete"
+ OnClick="deletePost"
+ Attributes.onclick="javascript:if(!confirm('Are you sure?')) return false;" />
+</com:TControl>
+
+<p>
+Author:
+<com:TLiteral Text="<%= $this->Post->author->username %>" /><br/>
+Time:
+<com:TLiteral Text="<%= date('m/d/Y h:m:sa', $this->Post->create_time) %>" />
+</p>
+
+<p>
+<com:TLiteral Text="<%= $this->Post->content %>" />
+</p>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.php
new file mode 100644
index 00000000..2aeaa4d3
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/ReadPost.php
@@ -0,0 +1,57 @@
+<?php
+
+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;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/config.xml b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/config.xml
new file mode 100644
index 00000000..64065ed5
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/posts/config.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <authorization>
+ <deny pages="NewPost,EditPost" roles="?" />
+ </authorization>
+</configuration> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.page
new file mode 100644
index 00000000..af03b858
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.page
@@ -0,0 +1,40 @@
+<%@ Title="My Blog - Manage User Accounts" %>
+
+<com:TContent ID="Main">
+
+<h1>Manage User Accounts</h1>
+
+<a href="<%= $this->Service->constructUrl('users.NewUser')%>">Create New User</a>
+<br/>
+
+<com:TDataGrid ID="UserGrid"
+ DataKeyField="username"
+ AutoGenerateColumns="false"
+ OnDeleteCommand="deleteButtonClicked">
+
+ <com:THyperLinkColumn
+ HeaderText="Username"
+ DataTextField="username"
+ DataNavigateUrlField="username">
+ <prop:DataNavigateUrlFormatString>#
+ $this->Service->constructUrl('users.EditUser',array('username'=>{0}))
+ </prop:DataNavigateUrlFormatString>
+ </com:THyperLinkColumn>
+
+ <com:TBoundColumn
+ HeaderText="Email"
+ DataField="email" />
+
+ <com:TCheckBoxColumn
+ HeaderText="Administrator"
+ DataField="role" />
+
+ <com:TButtonColumn
+ HeaderText="Command"
+ Text="Delete"
+ ButtonType="PushButton"
+ CommandName="delete" />
+
+</com:TDataGrid>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.php
new file mode 100644
index 00000000..ad8f6e3d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/AdminUser.php
@@ -0,0 +1,36 @@
+<?php
+
+class AdminUser extends TPage
+{
+ /**
+ * Populates the datagrid with user lists.
+ * This method is invoked by the framework when initializing the page
+ * @param mixed event parameter
+ */
+ public function onInit($param)
+ {
+ parent::onInit($param);
+ // fetches all data account information
+ $this->UserGrid->DataSource=UserRecord::finder()->findAll();
+ // binds the data to interface components
+ $this->UserGrid->dataBind();
+ }
+
+ /**
+ * Deletes a specified user record.
+ * This method responds to the datagrid's OnDeleteCommand event.
+ * @param TDataGrid the event sender
+ * @param TDataGridCommandEventParameter the event parameter
+ */
+ public function deleteButtonClicked($sender,$param)
+ {
+ // obtains the datagrid item that contains the clicked delete button
+ $item=$param->Item;
+ // obtains the primary key corresponding to the datagrid item
+ $username=$this->UserGrid->DataKeys[$item->ItemIndex];
+ // deletes the user record with the specified username primary key
+ UserRecord::finder()->deleteByPk($username);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.page
new file mode 100644
index 00000000..8aa3670e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.page
@@ -0,0 +1,61 @@
+<%@ Title="My Blog - Edit User" %>
+
+<com:TContent ID="Main">
+
+<h1>Edit User</h1>
+
+<span>Username:</span>
+<com:TLabel ID="Username" />
+
+<br/>
+<span>Password:</span>
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<span>Re-type Password:</span>
+<com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ErrorMessage="Your password entries did not match."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+
+<br/>
+<span>Email Address:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic" />
+<com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Email" />
+
+<com:TControl Visible="<%= $this->User->IsAdmin %>">
+<br/>
+<span>Role:</span>
+<br/>
+<com:TDropDownList ID="Role">
+ <com:TListItem Text="Normal User" Value="0" />
+ <com:TListItem Text="Administrator" Value="1" />
+</com:TDropDownList>
+</com:TControl>
+
+<br/>
+<span>First Name:</span>
+<br/>
+<com:TTextBox ID="FirstName" />
+
+<br/>
+<span>Last Name:</span>
+<br/>
+<com:TTextBox ID="LastName" />
+
+<br/>
+<com:TButton Text="Save" OnClick="saveButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.php
new file mode 100644
index 00000000..81538c33
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/EditUser.php
@@ -0,0 +1,83 @@
+<?php
+
+class EditUser extends TPage
+{
+ /**
+ * Initializes the inputs with existing user 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);
+ if(!$this->IsPostBack) // if the page is initially requested
+ {
+ // Retrieves the existing user data. This is equivalent to:
+ // $userRecord=$this->getUserRecord();
+ $userRecord=$this->UserRecord;
+
+ // Populates the input controls with the existing user data
+ $this->Username->Text=$userRecord->username;
+ $this->Email->Text=$userRecord->email;
+ $this->Role->SelectedValue=$userRecord->role;
+ $this->FirstName->Text=$userRecord->first_name;
+ $this->LastName->Text=$userRecord->last_name;
+ }
+ }
+
+ /**
+ * Saves the user account 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:
+ $userRecord=$this->UserRecord;
+
+ // Fetches the input data
+ $userRecord->username=$this->Username->Text;
+ // update password when the input is not empty
+ if(!empty($this->Password->Text))
+ $userRecord->password=$this->Password->Text;
+ $userRecord->email=$this->Email->Text;
+ // update the role if the current user is an administrator
+ if($this->User->IsAdmin)
+ $userRecord->role=(int)$this->Role->SelectedValue;
+ $userRecord->first_name=$this->FirstName->Text;
+ $userRecord->last_name=$this->LastName->Text;
+
+ // saves to the database via Active Record mechanism
+ $userRecord->save();
+
+ // redirects the browser to the homepage
+ $this->Response->redirect($this->Service->DefaultPageUrl);
+ }
+ }
+
+ /**
+ * Returns the user data to be editted.
+ * @return UserRecord the user data to be editted.
+ * @throws THttpException if the user data is not found.
+ */
+ protected function getUserRecord()
+ {
+ // the user to be editted is the currently logged-in user
+ $username=$this->User->Name;
+ // if the 'username' GET var is not empty and the current user
+ // is an administrator, we use the GET var value instead.
+ if($this->User->IsAdmin && $this->Request['username']!==null)
+ $username=$this->Request['username'];
+
+ // use Active Record to look for the specified username
+ $userRecord=UserRecord::finder()->findByPk($username);
+ if(!($userRecord instanceof UserRecord))
+ throw new THttpException(500,'Username is invalid.');
+ return $userRecord;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.page
new file mode 100644
index 00000000..f7fc7367
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.page
@@ -0,0 +1,28 @@
+<%@ Title="My Blog - Login" %>
+
+<com:TContent ID="Main">
+
+<h1>Login</h1>
+
+<span>Username:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ErrorMessage="Please provide your username."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Username" />
+
+<br/>
+<span>Password:</span>
+<com:TCustomValidator
+ ControlToValidate="Password"
+ ErrorMessage="Your entered an invalid password."
+ Display="Dynamic"
+ OnServerValidate="validateUser" />
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<com:TButton Text="Login" OnClick="loginButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.php
new file mode 100644
index 00000000..a0955490
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/LoginUser.php
@@ -0,0 +1,37 @@
+<?php
+
+class LoginUser extends TPage
+{
+ /**
+ * Validates whether the username and password are correct.
+ * This method responds to the TCustomValidator's OnServerValidate event.
+ * @param mixed event sender
+ * @param mixed event parameter
+ */
+ public function validateUser($sender,$param)
+ {
+ $authManager=$this->Application->getModule('auth');
+ if(!$authManager->login($this->Username->Text,$this->Password->Text))
+ $param->IsValid=false; // tell the validator that validation fails
+ }
+
+ /**
+ * Redirects the user's browser to appropriate URL if login succeeds.
+ * This method responds to the login button's OnClick event.
+ * @param mixed event sender
+ * @param mixed event parameter
+ */
+ public function loginButtonClicked($sender,$param)
+ {
+ if($this->Page->IsValid) // all validations succeed
+ {
+ // 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->DefaultPageUrl;
+ $this->Response->redirect($url);
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.page b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.page
new file mode 100644
index 00000000..d1547a9a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.page
@@ -0,0 +1,73 @@
+<%@ Title="My Blog - New User" %>
+
+<com:TContent ID="Main">
+
+<h1>Create New User</h1>
+
+<span>Username:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ErrorMessage="Please provide a username."
+ Display="Dynamic" />
+<com:TCustomValidator
+ ControlToValidate="Username"
+ ErrorMessage="Sorry, your username is taken by someone else. Please choose another username."
+ OnServerValidate="checkUsername"
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Username" />
+
+<br/>
+<span>Password:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="Password"
+ ErrorMessage="Please provide a password."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<span>Re-type Password:</span>
+<com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ErrorMessage="Your password entries did not match."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+
+<br/>
+<span>Email Address:</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic" />
+<com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic" />
+<br/>
+<com:TTextBox ID="Email" />
+
+<br/>
+<span>Role:</span>
+<br/>
+<com:TDropDownList ID="Role">
+ <com:TListItem Text="Normal User" Value="0" />
+ <com:TListItem Text="Administrator" Value="1" />
+</com:TDropDownList>
+
+<br/>
+<span>First Name:</span>
+<br/>
+<com:TTextBox ID="FirstName" />
+
+<br/>
+<span>Last Name:</span>
+<br/>
+<com:TTextBox ID="LastName" />
+
+<br/>
+<com:TButton Text="Create" OnClick="createButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.php b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.php
new file mode 100644
index 00000000..76e8cb88
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/NewUser.php
@@ -0,0 +1,45 @@
+<?php
+
+class NewUser extends TPage
+{
+ /**
+ * Checks whether the username exists in the database.
+ * This method responds to the OnServerValidate event of username's custom validator.
+ * @param mixed event sender
+ * @param mixed event parameter
+ */
+ public function checkUsername($sender,$param)
+ {
+ // valid if the username is not found in the database
+ $param->IsValid=UserRecord::finder()->findByPk($this->Username->Text)===null;
+ }
+
+ /**
+ * Creates a new user account 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 UserRecord object with user inputs
+ $userRecord=new UserRecord;
+ $userRecord->username=$this->Username->Text;
+ $userRecord->password=$this->Password->Text;
+ $userRecord->email=$this->Email->Text;
+ $userRecord->role=(int)$this->Role->SelectedValue;
+ $userRecord->first_name=$this->FirstName->Text;
+ $userRecord->last_name=$this->LastName->Text;
+
+ // saves to the database via Active Record mechanism
+ $userRecord->save();
+
+ // redirects the browser to the homepage
+ $this->Response->redirect($this->Service->DefaultPageUrl);
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/pages/users/config.xml b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/config.xml
new file mode 100644
index 00000000..56554441
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/pages/users/config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <authorization>
+ <allow pages="NewUser,AdminUser" roles="admin" />
+ <deny users="?" />
+ </authorization>
+</configuration> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day4/blog/protected/schema.sql b/demos/blog-tutorial/samples/day4/blog/protected/schema.sql
new file mode 100644
index 00000000..89f7388e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day4/blog/protected/schema.sql
@@ -0,0 +1,25 @@
+/* create users table */
+CREATE TABLE users (
+ username VARCHAR(128) NOT NULL PRIMARY KEY,
+ email VARCHAR(128) NOT NULL,
+ password VARCHAR(128) NOT NULL, /* in plain text */
+ role INTEGER NOT NULL, /* 0: normal user, 1: administrator */
+ first_name VARCHAR(128),
+ last_name VARCHAR(128)
+);
+
+/* create posts table */
+CREATE TABLE posts (
+ post_id INTEGER NOT NULL PRIMARY KEY,
+ author_id VARCHAR(128) NOT NULL
+ CONSTRAINT fk_author REFERENCES users(username),
+ create_time INTEGER NOT NULL, /* UNIX timestamp */
+ title VARCHAR(256) NOT NULL, /* title of the post */
+ content TEXT, /* post body */
+ status INTEGER NOT NULL /* 0: published; 1: draft; 2: pending; 2: denied */
+);
+
+/* insert some initial data records for testing */
+INSERT INTO users VALUES ('admin', 'admin@example.com', 'demo', 1, 'Qiang', 'Xue');
+INSERT INTO users VALUES ('demo', 'demo@example.com', 'demo', 0, 'Wei', 'Zhuo');
+INSERT INTO posts VALUES (NULL, 'admin', 1175708482, 'first post', 'this is my first post', 0);