summaryrefslogtreecommitdiff
path: root/demos/blog-tutorial
diff options
context:
space:
mode:
authorxue <>2007-04-08 21:33:23 +0000
committerxue <>2007-04-08 21:33:23 +0000
commitff32eed01f783ee33caeacb0f7315612f0994f8f (patch)
tree7b42ed14181d56632160f88f8063b54b17f176af /demos/blog-tutorial
parent773bf1d0299246d936dcad2ac2ca01bca9d64ca4 (diff)
Added Day 2 tutorial.
Diffstat (limited to 'demos/blog-tutorial')
-rw-r--r--demos/blog-tutorial/protected/common/TopicList.tpl28
-rw-r--r--demos/blog-tutorial/protected/pages/Day1/CreateContact.page58
-rw-r--r--demos/blog-tutorial/protected/pages/Day1/Setup.page11
-rw-r--r--demos/blog-tutorial/protected/pages/Day1/ShareLayout.page4
-rw-r--r--demos/blog-tutorial/protected/pages/Day1/output.gifbin15045 -> 13379 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day2/CreateAR.page2
-rw-r--r--demos/blog-tutorial/protected/pages/Day2/CreateDB.page12
-rw-r--r--demos/blog-tutorial/protected/pages/Day2/ER.gifbin4919 -> 5172 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day2/ER.vsdbin73216 -> 73216 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/Auth.page102
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page148
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page191
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page158
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page206
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/Overview.page26
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/directories.gifbin0 -> 10329 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/output.gifbin0 -> 10006 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/output2.gifbin0 -> 9222 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Day3/output3.gifbin0 -> 9464 bytes
-rw-r--r--demos/blog-tutorial/protected/pages/Requirements.page2
-rw-r--r--demos/blog-tutorial/samples/day1/blog/protected/layouts/MainLayout.tpl2
-rw-r--r--demos/blog-tutorial/samples/day2/blog/protected/data/blog.dbbin5120 -> 4096 bytes
-rw-r--r--demos/blog-tutorial/samples/day2/blog/protected/database/PostRecord.php4
-rw-r--r--demos/blog-tutorial/samples/day2/blog/protected/database/UserRecord.php2
-rw-r--r--demos/blog-tutorial/samples/day2/blog/protected/layouts/MainLayout.tpl2
-rw-r--r--demos/blog-tutorial/samples/day2/blog/protected/schema.sql9
-rw-r--r--demos/blog-tutorial/samples/day3/blog/index.php23
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/.htaccess1
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/BlogUser.php59
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/application.xml55
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/data/blog.dbbin0 -> 5120 bytes
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/database/PostRecord.php27
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/database/UserRecord.php27
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.php19
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.tpl29
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/Contact.page47
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/Contact.php30
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/Home.page7
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/AdminUser.page40
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/AdminUser.php36
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.page61
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php83
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.page28
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php37
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.page73
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php45
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/pages/users/config.xml7
-rw-r--r--demos/blog-tutorial/samples/day3/blog/protected/schema.sql24
-rw-r--r--demos/blog-tutorial/themes/PradoSoft/style.css14
49 files changed, 1689 insertions, 50 deletions
diff --git a/demos/blog-tutorial/protected/common/TopicList.tpl b/demos/blog-tutorial/protected/common/TopicList.tpl
index b6174985..f66a2d0b 100644
--- a/demos/blog-tutorial/protected/common/TopicList.tpl
+++ b/demos/blog-tutorial/protected/common/TopicList.tpl
@@ -18,7 +18,7 @@
</div>
<div class="topic">
-<div>Day 2: Working with Database - Part I</div>
+<div>Day 2: Setting Up Database</div>
<ul>
<li><a href="?page=Day2.CreateDB">Creating Database</a></li>
<li><a href="?page=Day2.ConnectDB">Establishing DB Connection</a></li>
@@ -27,26 +27,34 @@
</div>
<div class="topic">
-<div>Day 3: Working with Database - Part II</div>
+<div>Day 3: Implementing User Management</div>
<ul>
- <li><a href="?page=">Creating NewPost Page</a></li>
- <li><a href="?page=">Creating ListPost Page</a></li>
+ <li><a href="?page=Day3.Overview">Overview</a></li>
+ <li><a href="?page=Day3.Auth">Authentication and Authorization</a></li>
+ <li><a href="?page=Day3.CreateLoginUser">Creating LoginUser Page</a></li>
+ <li><a href="?page=Day3.CreateNewUser">Creating NewUser Page</a></li>
+ <li><a href="?page=Day3.CreateEditUser">Creating EditUser Page</a></li>
+ <li><a href="?page=Day3.CreateAdminUser">Creating AdminUser Page</a></li>
</ul>
</div>
<div class="topic">
-<div>Day 4: Authentication and Authorization</div>
+<div>Day 4: Implementing Post Management</div>
<ul>
- <li><a href="?page=">Creating Login Page</a></li>
- <li><a href="?page=">Using PRADO Auth Framework</a></li>
- <li><a href="?page=">Database-Driven Authentication</a></li>
+ <li><a href="?page=Day4.Overview">Overview</a></li>
+ <li><a href="?page=Day4.CreateReadPost">Creating ReadPost Page</a></li>
+ <li><a href="?page=Day4.CreateListPost">Creating ListPost Page</a></li>
+ <li><a href="?page=Day4.CreateNewPost">Creating NewPost Page</a></li>
+ <li><a href="?page=Day4.CreateEditPost">Creating EditPost Page</a></li>
+ <li><a href="?page=Day4.CreateAdminPost">Creating AdminPost Page</a></li>
</ul>
</div>
<div class="topic">
-<div>Day 5: Developing and Using Components</div>
+<div>Day 5: Creating Portlets</div>
<ul>
-</ul>
+ <li><a href="?page=Day5.Create">Overview</a></li>
+ <li><a href="?page=Day4.CreateReadPost">Creating ReadPost Page</a></li>
</div>
<div class="topic">
diff --git a/demos/blog-tutorial/protected/pages/Day1/CreateContact.page b/demos/blog-tutorial/protected/pages/Day1/CreateContact.page
index 07adbe93..4daf43e3 100644
--- a/demos/blog-tutorial/protected/pages/Day1/CreateContact.page
+++ b/demos/blog-tutorial/protected/pages/Day1/CreateContact.page
@@ -14,7 +14,7 @@ The purpose of the <tt>Contact</tt> page is to collect feedback from Web users o
To create the <tt>Contact</tt> page, we need two files under the <tt>pages</tt> directory: the page template file <tt>Contact.page</tt> and the page class file <tt>Contact.php</tt>.
</p>
-<img src="<%~ directories2.gif %>" />
+<img src="<%~ directories2.gif %>" class="output" />
<com:InfoBox>
A <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Pages">page</a> must have either a <a href="http://www.pradosoft.com/demos/quickstart/?page=Configurations.Templates1">template</a> file (extension <tt>.page</tt>) or a class file, or both:
@@ -35,7 +35,7 @@ We first create the template file for the <tt>Contact</tt> page.
</p>
<p>
-We use template to organize the presentational layout of the feedback form. In the template, we use <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.TextBox">textboxes</a> to collect user's name, email and feedback. And we use <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Validation">validators</a> to ensure that the user provides all these information before submitting the feedback form. The whole template looks like the following:
+We use template to organize the presentational layout of the feedback form. In the template, we use <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.TextBox">textboxes</a> to collect user's name, email and feedback. And we use <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Validation">validators</a> to ensure that the user provides all these information before submitting the feedback form. The whole template is as follows,
</p>
<com:TTextHighlighter CssClass="source" Language="prado">
@@ -47,13 +47,40 @@ We use template to organize the presentational layout of the feedback form. In t
&lt;com:TForm>
- ...textbox and validator for user's name...
+<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>
+&lt;com:TRequiredFieldValidator ControlToValidate="Name"
+ ErrorMessage="Please provide your name."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Name" />
- ...textbox and validators for user's email...
+<br/>
+<span>Your Email:</span>
+&lt;com:TRequiredFieldValidator ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic" />
+&lt;com:TEmailAddressValidator ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Email" />
- ...textbox and validator for user's feedback content...
+<br/>
+<span>Feedback:</span>
+&lt;com:TRequiredFieldValidator ControlToValidate="Feedback"
+ ErrorMessage="Please provide your feedback."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Feedback"
+ TextMode="MultiLine"
+ Rows="10"
+ Columns="40" />
- &lt;com:TButton Text="Submit" OnClick="submitButtonClicked" />
+<br/>
+&lt;com:TButton Text="Submit" OnClick="submitButtonClicked" />
&lt;/com:TForm>
@@ -62,7 +89,7 @@ We use template to organize the presentational layout of the feedback form. In t
</com:TTextHighlighter>
<p>
-The template looks very similar to a normal HTML page. The main difference is that the template contains a few <tt>&lt;com:&gt;</tt> tags. Each <tt>&lt;com:&gt;</tt> tag refers to a <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Controls">control</a> whose properties are being initialized with name-value pairs in the tag. For example, the <tt>&lt;com:TButton&gt;</tt> refers to the <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Button">TButton</a> control which displays a button that users can click on to submit the feedback form. For complete template syntax, please refer to the <a href="http://www.pradosoft.com/demos/quickstart/?page=Configurations.Templates1">Quickstart Tutorial</a>.
+As we can see that the template looks very similar to a normal HTML page. The main difference is that the template contains a few <tt>&lt;com:&gt;</tt> tags. Each <tt>&lt;com:&gt;</tt> tag refers to a <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Controls">control</a> whose properties are being initialized with name-value pairs in the tag. For example, the <tt>&lt;com:TButton&gt;</tt> refers to the <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Button">TButton</a> control which displays a button that users can click on to submit the feedback form. For complete template syntax, please refer to the <a href="http://www.pradosoft.com/demos/quickstart/?page=Configurations.Templates1">Quickstart Tutorial</a>.
</p>
<com:InfoBox>
@@ -70,7 +97,7 @@ PRADO provides a control for every type of HTML input. For example, <a href="htt
</com:InfoBox>
<p>
-The following template shows the detail about "...textbox and validators for user's email..." in the above.
+Besides <tt>TTextBox</tt> controls, the template also uses many validator controls which ensure user's inputs satisfy specific validation rules. For example, to ensure a legitimate email address is provided, we use two validators to validate the "email" text box, as shown in the following:
</p>
<com:TTextHighlighter CssClass="source" Language="prado">
@@ -78,25 +105,24 @@ The following template shows the detail about "...textbox and validators for use
&lt;com:TRequiredFieldValidator
ControlToValidate="Email"
ErrorMessage="Please provide your email address."
- Display="Dynamic"
- />
+ Display="Dynamic" />
&lt;com:TEmailAddressValidator
ControlToValidate="Email"
ErrorMessage="You entered an invalid email address."
- Display="Dynamic"
- />
+ Display="Dynamic" />
<br/>
&lt;com:TTextBox ID="Email" />
<br/>
</com:TTextHighlighter>
<p>
-Three controls are used here:
+Below we summarize the controls that are used in the page template:
</p>
<ul>
-<li><a href="http://www.pradosoft.com/docs/classdoc/TTextBox">TTextBox</a> displays a textbox to allow user to enter his email address.</li>
-<li><a href="http://www.pradosoft.com/docs/classdoc/TRequiredFieldValidator">TRequiredFieldValidator</a> ensures that the textbox is not empty when the feedback is submitted.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TForm">TForm</a> displays an HTML form. Any input control must be enclosed within it. And most importantly, at most one <tt>TForm</tt> may appear in a page.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TTextBox">TTextBox</a> displays a text box to collect user text input.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TRequiredFieldValidator">TRequiredFieldValidator</a> ensures that the associated text box is not empty when the feedback is submitted.</li>
<li><a href="http://www.pradosoft.com/docs/classdoc/TEmailAddressValidator">TEmailAddressValidator</a> ensures that the textbox contains a <i>valid</i> email address when the feedback is submitted.</li>
</ul>
@@ -168,7 +194,7 @@ Page class name must be the same as the file name. This is also a requirement fo
Our newly created <tt>Contact</tt> can be tested via the URL <tt>http://hostname/blog/index.php?page=Contact</tt>. If we click on the submit button without entering any information, we will see error messages appearing next to the corresponding textboxes. If we enter all required information, the method <tt>mailFeedback()</tt> will be invoked.
</p>
-<img src="<%~ output.gif %>" />
+<img src="<%~ output.gif %>" class="output" />
<p>
A further enhancement to this page is to show some confirmation message on the page after the user submits feedback. And possibly, the browser may be redirected to another page if the submission is successful. We will leave these tasks to our readers.
diff --git a/demos/blog-tutorial/protected/pages/Day1/Setup.page b/demos/blog-tutorial/protected/pages/Day1/Setup.page
index ee4744f5..0fe877bf 100644
--- a/demos/blog-tutorial/protected/pages/Day1/Setup.page
+++ b/demos/blog-tutorial/protected/pages/Day1/Setup.page
@@ -21,7 +21,7 @@ php path/to/prado-cli.php -c .
Running the above command creates the following directories and files:
</p>
-<img src="<%~ directories.gif %>" />
+<img src="<%~ directories.gif %>" class="output" />
<p>
We now have a skeleton PRADO application accessible via the URL <tt>http://hostname/blog/index.php</tt> which brings up a Web page showing "Welcome to PRADO".
@@ -145,13 +145,16 @@ To change the location of the root page directory and change the name of homepag
</p>
<com:TTextHighlighter CssClass="source" Language="xml">
-<services>
- <service id="page"
+<?xml version="1.0" encoding="utf-8"?>
+<application id="blog" mode="Debug">
+ <services>
+ <service id="page"
class="TPageService"
BasePath="path.to.pages"
DefaultPage="NewHome"
/>
-</services>
+ </services>
+</application>
</com:TTextHighlighter>
<p>
diff --git a/demos/blog-tutorial/protected/pages/Day1/ShareLayout.page b/demos/blog-tutorial/protected/pages/Day1/ShareLayout.page
index 548cec1c..d3d1f553 100644
--- a/demos/blog-tutorial/protected/pages/Day1/ShareLayout.page
+++ b/demos/blog-tutorial/protected/pages/Day1/ShareLayout.page
@@ -17,7 +17,7 @@ It is also possible to share common layout via <a href="http://www.pradosoft.com
We now create the master control <tt>MainLayout</tt> to represent the common layout shared by our blog pages. The <tt>MainLayout</tt> control is a template control extending from <tt>TTemplateControl</tt>. It requires a template file <tt>MainLayout.tpl</tt> and a class file <tt>MainLayout.php</tt> located under the same directory. To facilitate maintenance, we create a new directory <tt>protected/layouts</tt> to hold them.
</p>
-<img src="<%~ directories3.gif %>" />
+<img src="<%~ directories3.gif %>" class="output" />
<p>
For the moment, <tt>MainLayout</tt> only contains a simple header and a footer, as shown in the following. In future, we will add a side-bar to it. Readers are also encouraged to enhance the layout with other features.
@@ -38,7 +38,7 @@ For the moment, <tt>MainLayout</tt> only contains a simple header and a footer,
</div>
<div id="footer">
-Powered by &lt;%= PRADO::poweredByPrado() %>
+&lt;%= PRADO::poweredByPrado() %>
</div>
&lt;/com:TForm>
diff --git a/demos/blog-tutorial/protected/pages/Day1/output.gif b/demos/blog-tutorial/protected/pages/Day1/output.gif
index f3ece514..9ad2bfb8 100644
--- a/demos/blog-tutorial/protected/pages/Day1/output.gif
+++ b/demos/blog-tutorial/protected/pages/Day1/output.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day2/CreateAR.page b/demos/blog-tutorial/protected/pages/Day2/CreateAR.page
index d8b8b8ce..00ac1166 100644
--- a/demos/blog-tutorial/protected/pages/Day2/CreateAR.page
+++ b/demos/blog-tutorial/protected/pages/Day2/CreateAR.page
@@ -61,7 +61,7 @@ Here we used the <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundam
We should see the following directory structure with two new files under <tt>protected/database</tt>:
</p>
-<img src="<%~ directories2.gif %>" />
+<img src="<%~ directories2.gif %>" class="output" />
<p>
If we check the <tt>PostRecord</tt> class file, we should see the following content.
diff --git a/demos/blog-tutorial/protected/pages/Day2/CreateDB.page b/demos/blog-tutorial/protected/pages/Day2/CreateDB.page
index 291b20e4..eebda4c1 100644
--- a/demos/blog-tutorial/protected/pages/Day2/CreateDB.page
+++ b/demos/blog-tutorial/protected/pages/Day2/CreateDB.page
@@ -10,7 +10,7 @@ Most Web applications use database to keep data. Our blog system is not an excep
For tutorial purpose, we have simplified the requirements of our blog system so that it only needs to deal with user and post data. We thus create two database tables, <tt>users</tt> and <tt>posts</tt>, as shown in the following entity-relationship (ER) diagram.
</p>
-<img src="<%~ ER.gif %>" />
+<img src="<%~ ER.gif %>" class="output" />
<p>
We use a SQLite 3 database to keep our data. We first convert the ER diagram into the following SQL statements and save them in the file <tt>protected/schema.sql</tt>.
@@ -20,8 +20,8 @@ We use a SQLite 3 database to keep our data. We first convert the ER diagram int
/* create users table */
CREATE TABLE users (
username VARCHAR(128) NOT NULL PRIMARY KEY,
- email VARCHAR(128) NOT NULL UNIQUE,
- password VARCHAR(128) NOT NULL, /* plain text password */
+ 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)
@@ -33,13 +33,13 @@ CREATE TABLE posts (
author VARCHAR(128) NOT NULL, /* references users.username */
create_time INTEGER NOT NULL, /* UNIX timestamp */
title VARCHAR(256) NOT NULL, /* title of the post */
- content TEXT NOT NULL /* content of the post */
+ 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');
+INSERT INTO posts VALUES (NULL, 'admin', 1175708482, 'first post', 'this is my first post', 0);
</com:TTextHighlighter>
<com:NoteBox>
@@ -58,7 +58,7 @@ sqlite3 blog.db < ../schema.sql
The database has been created as <tt>protected/data/blog.db</tt> and we shall see the following directories and files:
</p>
-<img src="<%~ directories.gif %>" />
+<img src="<%~ directories.gif %>" class="output" />
<com:NoteBox>
It is required by SQLite that both the directory <tt>protected/data</tt> and the database file <tt>protected/data/blog.db</tt> be set writable by the Web server process.
diff --git a/demos/blog-tutorial/protected/pages/Day2/ER.gif b/demos/blog-tutorial/protected/pages/Day2/ER.gif
index 13e2d15b..90e4c1ea 100644
--- a/demos/blog-tutorial/protected/pages/Day2/ER.gif
+++ b/demos/blog-tutorial/protected/pages/Day2/ER.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day2/ER.vsd b/demos/blog-tutorial/protected/pages/Day2/ER.vsd
index 2b59897a..95cf7f32 100644
--- a/demos/blog-tutorial/protected/pages/Day2/ER.vsd
+++ b/demos/blog-tutorial/protected/pages/Day2/ER.vsd
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day3/Auth.page b/demos/blog-tutorial/protected/pages/Day3/Auth.page
new file mode 100644
index 00000000..82c667d7
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/Auth.page
@@ -0,0 +1,102 @@
+<com:TContent ID="Main">
+
+<h1>Authentication and Authorization</h1>
+
+<p>
+Before we set off to implement the user pages, we need to do some work to enable <a href="http://www.pradosoft.com/demos/quickstart/index.php?page=Advanced.Auth">authentication and authorization</a>.
+</p>
+
+<p>
+We add two new modules to the application configuration as follows:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="xml">
+<modules>
+ ...TDataSourceConfig and TActiveRecordConfig modules...
+
+ <module id="auth"
+ class="System.Security.TAuthManager"
+ UserManager="users"
+ LoginPage="users.LoginUser" />
+
+ <module id="users"
+ class="System.Security.TDbUserManager"
+ UserClass="Application.BlogUser" />
+</modules>
+</com:TTextHighlighter>
+
+<p>
+The <a href="http://www.pradosoft.com/docs/classdoc/TAuthManager">TAuthManager</a> module manages the whole authentication and authorization workflow. It uses the <tt>users</tt> module as its user manager (see below). By specifying the <tt>LoginPage</tt> property, we inform the auth manager to redirect user's browser to the <tt>LoginUser</tt> page when an authorization fails. We will describe how to create <tt>LoginUser</tt> in the next subsection.
+</p>
+
+<p>
+The <tt>user</tt> module is of class <a href="http://www.pradosoft.com/docs/classdoc/TDbUserManager">TDbUserManager</a> which is responsible to verify the validity of a user and keep basic user data in the PHP session. The <tt>UserClass</tt> property is initialized as <tt>Application.BlogUser</tt>, which indicates the user manager would look for a <tt>BlogUser</tt> class under the directory <tt>protected</tt> (remember the alias <tt>Application</tt> refers to the <tt>protected</tt> directory) and use it to keep user's session data.
+</p>
+
+<p>
+As we will see in later sections, in controls and pages, we can use <tt>$this->User</tt> to obtain the <tt>BlogUser</tt> object which contains the information of the user currently accessing the system.
+</p>
+
+<p>
+Below is the implementation detail of <tt>BlogUser</tt>. Notice <a href="http://www.pradosoft.com/demos/quickstart/index.php?page=Database.ActiveRecord">Active Record</a> is used to perform DB query. For example, we use <tt>UserRecord::finder()->findByPk($username)</tt> to look for the primary key specified by <tt>$username</tt> in the <tt>users</tt> table.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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');
+ }
+}
+</com:TTextHighlighter>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page
new file mode 100644
index 00000000..36b43014
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page
@@ -0,0 +1,148 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>AdminUser</tt> Page</h1>
+
+<p>
+The <tt>AdminUser</tt> page displays all user accounts in a list so that the administrator can perform some administrative work. For simplicity, the administrative work our blog system supports include editting a user account and deleting a user account.
+</p>
+
+<p>
+We will display the user list in a table. Each row of the table represents a single user account, and the following columns are to be displayed:
+</p>
+<ul>
+<li>Username - displays the usernames. In each cell a hyerplink is displayed which leads to the corresponding <a href="?page=Day3.CreateEditUser">EditUser</a> page.</li>
+<li>Email - displays the emails.</li>
+<li>Administrator - shows whether the user account is of the administrator role.</li>
+<li>Command - displays a column of "Delete" buttons. Clicking on any of them will lead to deletion of the corresponding user account.</li>
+</ul>
+
+
+<h2>Creating Page Template</h2>
+<p>
+We use <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.DataGrid">TDataGrid</a> to display the user accounts. Based on the above analysis, we configure the following four columns:
+</p>
+<ul>
+<li><a href="http://www.pradosoft.com/docs/classdoc/THyperLinkColumn">THyperLinkColumn</a> displays the username column. The URL is constructed according to the PHP expression specified in the <tt>DataNavigateUrlFormatString</tt> property.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TBoundColumn">TBoundColumn</a> displays the email column.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TCheckBoxColumn">TCheckBoxColumn</a> uses checkboxes to indicate whether a user account is of role administrator.</li>
+<li><a href="http://www.pradosoft.com/docs/classdoc/TButtonColumn">TButtonColumn</a> displays a column of "Delete" buttons.</li>
+</ul>
+
+<p>Complete page template is shown as follows:</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - Manage User Accounts" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Manage User Accounts</h1>
+
+<a href="&lt;%= $this->Service->constructUrl('users.NewUser')%>">Create New User</a>
+<br/>
+
+&lt;com:TDataGrid ID="UserGrid"
+ DataKeyField="username"
+ AutoGenerateColumns="false"
+ OnDeleteCommand="deleteButtonClicked">
+
+ &lt;com:THyperLinkColumn
+ HeaderText="Username"
+ DataTextField="username"
+ DataNavigateUrlField="username">
+ &lt;prop:DataNavigateUrlFormatString>#
+ $this->Service->constructUrl('users.EditUser',array('username'=>{0}))
+ &lt;/prop:DataNavigateUrlFormatString>
+ &lt;/com:THyperLinkColumn>
+
+ &lt;com:TBoundColumn
+ HeaderText="Email"
+ DataField="email" />
+
+ &lt;com:TCheckBoxColumn
+ HeaderText="Administrator"
+ DataField="role" />
+
+ &lt;com:TButtonColumn
+ HeaderText="Command"
+ Text="Delete"
+ ButtonType="PushButton"
+ CommandName="delete" />
+
+&lt;/com:TDataGrid>
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+In the above page template, the datagrid's <tt>OnDeleteCommand</tt> event is ttached with the method <tt>deleteButtonClicked()</tt> which we shall implement in the page class. In addition, the datagrid needs to be populated with user accounts data when the page is initialized. Therefore, we write the page class as follows:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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);
+ }
+}
+</com:TTextHighlighter>
+
+<p>
+In the above, the <tt>deleteButtonClicked()</tt> method is invoked whenever a "Delete" button is clicked. To determine which row of the buttons is clicked, we check the <tt>Item.ItemIndex</tt> property of the event parameter. To further identify which user account is to be deleted, we retrieve the primary key (username) value via the datagrid's <tt>DataKeys</tt> property.
+</p>
+
+<com:TipBox>
+All <a href="http://www.pradosoft.com/docs/classdoc/TDataBoundControl">data-bound</a> controls have similar usage pattern. That is, set the <tt>DataSource</tt> property with the data and call <tt>dataBind()</tt> method to binds the data to the control's internal structure.
+</com:TipBox>
+
+
+<h2>Adding Permission Check</h2>
+<p>
+Since <tt>AdminUser</tt> should only be accessible by administrators, we need to adjust the page configuration file <tt>protected/pages/users/config.xml</tt> accordingly.
+</p>
+<com:TTextHighlighter CssClass="source" Language="xml">
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <authorization>
+ <allow pages="NewUser,AdminUser" roles="admin" />
+ <deny users="?" />
+ </authorization>
+</configuration>
+</com:TTextHighlighter>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>AdminUser</tt> page, visit the URL <tt>http://hostname/blog/index.php?page=users.AdminUser</tt>. You may be required to login as an administrator first if you have not done so. We shall expect to see the following result.
+</p>
+
+<img src="<%~ output3.gif %>" class="output" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page
new file mode 100644
index 00000000..64ac6798
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page
@@ -0,0 +1,191 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>EditUser</tt> Page</h1>
+
+<p>
+The <tt>EditUser</tt> page is very similar to the <a href="?page=Day3.CreateNewUser">NewUser</a>. The main difference is that when <tt>EditUser</tt> is initially requested, the input fields should be initialized with existing user information. Another slight difference is that <tt>EditUser</tt> can also be accessed by normal users.
+</p>
+
+<p>
+To determine which user account is to be editted, we use the following policy:
+</p>
+<ul>
+<li>If the current user is an administrator, he can edit any user account by specifying the account's username in a GET variable named 'username'. For example, <tt>http://hostname/blog/index.php?page=users.EditUser&username=demo</tt>.</li>
+<li>If the current user is an administrator and the URl does not contain 'username', the administrator himself's data is being updated.</li>
+<li>If the current user is a normal user, he can only edit his own account information, and he cannot modify his role data.</li>
+</ul>
+
+<h2>Creating Page Template</h2>
+<p>
+As you may have guessed, the page template <tt>EditUser</tt> is largely the same as that of <tt>NewUser</tt>. Besides the difference in page title and the caption of the submit button, there are three main differences.
+</p>
+<ul>
+<li>The "username" text box is replaced by a <a href="http://www.pradosoft.com/demos/quickstart/?page=Controls.Label">TLabel</a> control because we do not allow modifying username;</li>
+<li>The validator for the "password" input is removed. This is because if the user does not provide a password during editting, it means the user does not want to change the password.</li>
+<li>The "role" input is surrounded with <tt>TControl</tt> whose visibility is toggled according to the role of the currently logged-in user. If the user is not an administrator, the "role" input will not be displayed because normal users are not allowed to modify their roles.</li>
+</ul>
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - Edit User" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Edit User</h1>
+
+<span>Username:</span>
+&lt;com:TLabel ID="Username" />
+
+<br/>
+<span>Password:</span>
+<br/>
+&lt;com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<span>Re-type Password:</span>
+&lt;com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ErrorMessage="Your password entries did not match."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Password2" TextMode="Password" />
+
+<br/>
+<span>Email Address:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic" />
+&lt;com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Email" />
+
+&lt;com:TControl Visible="&lt;%= $this->User->IsAdmin %>">
+<br/>
+<span>Role:</span>
+<br/>
+&lt;com:TDropDownList ID="Role">
+ &lt;com:TListItem Text="Normal User" Value="0" />
+ &lt;com:TListItem Text="Administrator" Value="1" />
+&lt;/com:TDropDownList>
+&lt;/com:TControl>
+
+<br/>
+<span>First Name:</span>
+<br/>
+&lt;com:TTextBox ID="FirstName" />
+
+<br/>
+<span>Last Name:</span>
+<br/>
+&lt;com:TTextBox ID="LastName" />
+
+<br/>
+&lt;com:TButton Text="Save" OnClick="saveButtonClicked" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+
+<h2>Creating Page Class</h2>
+
+<p>
+Based on the above description and template, we need to write a page class that initializes the inputs with the existing user information. In addition, the page class also needs to implement the <tt>saveButtonClicked()</tt> method which is attached to the "save" button's <tt>OnClick</tt> event.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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->constructUrl($this->Service->DefaultPage));
+ }
+ }
+
+ /**
+ * 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;
+ }
+}
+</com:TTextHighlighter>
+
+<com:TipBox>
+The <tt>onInit()</tt> method is invoked by PRADO during one of the <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Pages">page lifecycles</a>. Other commonly overriden lifecycle methods include <tt>onPreInit()</tt>, <tt>onLoad()</tt> and <tt>onPreRender()</tt>.
+</com:TipBox>
+
+<h2>Testing</h2>
+<p>
+To test the <tt>EditUser</tt> page, visit the URL <tt>http://hostname/blog/index.php?page=users.EditUser&username=demo</tt>. You may be required to login first if you have not done so. Try logging in with different accounts (e.g. admin/demo, demo/demo) and see how the page displays differently.
+</p>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page
new file mode 100644
index 00000000..61ce27b7
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page
@@ -0,0 +1,158 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>LoginUser</tt> Page</h1>
+
+<p>
+The <tt>LoginUser</tt> page displays a login form and authenticates a user who tries to login. As described in <a href="?page=Day3.Auth">authentication and authorization</a>, the user's browser is automatically redirected to the <tt>LoginUser</tt> page when the user is attempting to access a privileged page, such as a user admin page.
+</p>
+
+<p>
+The workflow of <tt>LoginUser</tt> is very similar to the <a href="?page=Day1.CreateContact">Contact</a> page:
+</p>
+<ol>
+<li>When a user accesses the <tt>LoginUser</tt> page, a login form is displayed;</li>
+<li>The user fills in the username and password and clicks on the "login" button;</li>
+<li>The <tt>LoginUser</tt> receives the "login" event and triggers the authentication sequence;</li>
+<li>If the user enters correct username and password, the system assigns him a valid identity and redirects his browser to the desired privileged page; If not, a "password invalid" message is displayed.
+</ol>
+
+<h2>Creating Page Template</h2>
+
+<p>
+Below we show the template for <tt>LoginPage</tt>. As we see, the page mainly contains a text box for collecting username and a text box for password. The username input is required, which is ensured by the <tt>TRequiredFieldValidator</tt>. The correctness of the password input is ensured by the <a href="http://www.pradosoft.com/demos/quickstart/index.php?page=Controls.Validation">TCustomValidator</a> which invokes the page's <tt>validateUser()</tt> method when validation is performed. The page also has "login" button which invokes the page's <tt>loginButtonClicked()</tt> when it is clicked.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - Login" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Login</h1>
+
+<span>Username:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ErrorMessage="Please provide your username."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Username" />
+
+<br/>
+<span>Password:</span>
+&lt;com:TCustomValidator
+ ControlToValidate="Password"
+ ErrorMessage="Your entered an invalid password."
+ Display="Dynamic"
+ OnServerValidate="validateUser" />
+<br/>
+&lt;com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+&lt;com:TButton Text="Login" OnClick="loginButtonClicked" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+<h2>Creating Page Class</h2>
+
+<p>
+Like the <a href="?page=Day1.CreateContact">Contact</a> page, the <tt>LoginUser</tt> page also needs a class file which mainly contains the implementation of event handlers attached in the page template. Here, we need to implement two methods: <tt>validateUser()</tt> and <tt>loginButtonClicked()</tt>. In <tt>validateUser()</tt>, we use the <a href="?page=Day3.Auth">auth manager</a> to verify if the username and password are valid. If valid, the auth manager will automatically create a user session with appropriate user identity information.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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->constructUrl($this->Service->DefaultPage);
+ $this->Response->redirect($url);
+ }
+ }
+}
+</com:TTextHighlighter>
+
+
+<h2>Testing</h2>
+
+<p>
+So we have created the <tt>LoginUser</tt> page. We can test it by visiting the URL <tt>http://hostname/blog/index.php?page=users.LoginUser</tt>. Remember in the <a href="?page=Day2.CreateDB">Creating Database</a> subsection, we already created two user accounts (username/password): <tt>admin/demo</tt> and <tt>demo/demo</tt>. We can use them to test our login page.
+</p>
+
+<img src="<%~ output.gif %>" class="output"/>
+
+<h2>Adding Login/Logout Links to Master</h2>
+
+<p>
+To provide a direct way for users to login and logout, we modify the <tt>MainLayout</tt> master control a bit. In particular, we add a "login" hyperlink which links to the <tt>LoginUser</tt> page. We also add a "logout" link button which logs out a user when it is clicked.
+</p>
+
+<p>
+We modify the footer section of the <tt>MainLayout</tt>'s template as follows. The visibility of "login" and "logout" is determined according to user's status. If the user is not logged in yet, i.e., <tt>$this->User->IsGuest</tt> is true, the "login" link is visible while the "logout" link is not; and vice versa.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+<div id="footer">
+&lt;com:THyperLink Text="Login"
+ NavigateUrl="&lt;%= $this->Service->constructUrl('users.LoginUser') %>"
+ Visible="&lt;%= $this->User->IsGuest %>" />
+
+&lt;com:TLinkButton Text="Logout"
+ OnClick="logoutButtonClicked"
+ Visible="&lt;%= !$this->User->IsGuest %>" />
+
+<br/>
+&lt;%= PRADO::poweredByPrado() %>
+</div>
+</com:TTextHighlighter>
+
+<p>
+Since the "logout" button attaches its <tt>OnClick</tt> event with a method called <tt>logoutButtonClicked()</tt>, we need to modify the class file of <tt>MainLayout</tt> as well.
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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);
+ }
+}
+</com:TTextHighlighter>
+
+<p>
+Now if we visit any page of our blog system, we should see either a link at the bottom of the page. The link displays "Login" if we have not logged in yet and "Logout" if we have logged in. If we click on "Logout", the browser will be redirected to the homepage and "Login" is displayed meaning we have logged out.
+</p>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page
new file mode 100644
index 00000000..d9ef684f
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page
@@ -0,0 +1,206 @@
+<com:TContent ID="Main">
+
+<h1>Creating <tt>NewUser</tt> Page</h1>
+
+<p>
+The <tt>NewUser</tt> page is provided to the administrator user to create new a new user account. It needs to display a form that collects the information about the new user account. According to our <a href="?page=Day2.CreateDB">database definition</a>, we will need to collect the following information:
+</p>
+
+<ul>
+<li><tt>username</tt> - string, required and unique</li>
+<li><tt>email</tt> - string, required and unique</li>
+<li><tt>password</tt> - string, required</li>
+<li><tt>role</tt> - integer, required (either 0 or 1)</li>
+<li><tt>first_name</tt> - string, optional</li>
+<li><tt>last_name</tt> - string, optional</li>
+</ul>
+
+<h2>Creating Page Template</h2>
+<p>
+Based on the above analysis, we write the page template as follows:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="prado">
+&lt;%@ Title="My Blog - New User" %>
+
+&lt;com:TContent ID="Main">
+
+<h1>Create New User</h1>
+
+<span>Username:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ErrorMessage="Please provide a username."
+ Display="Dynamic" />
+&lt;com:TCustomValidator
+ ControlToValidate="Username"
+ ErrorMessage="Sorry, your username is taken by someone else. Please choose another username."
+ OnServerValidate="checkUsername"
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Username" />
+
+<br/>
+<span>Password:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="Password"
+ ErrorMessage="Please provide a password."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<span>Re-type Password:</span>
+&lt;com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ErrorMessage="Your password entries did not match."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Password2" TextMode="Password" />
+
+<br/>
+<span>Email Address:</span>
+&lt;com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ErrorMessage="Please provide your email address."
+ Display="Dynamic" />
+&lt;com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ErrorMessage="You entered an invalid email address."
+ Display="Dynamic" />
+<br/>
+&lt;com:TTextBox ID="Email" />
+
+<br/>
+<span>Role:</span>
+<br/>
+&lt;com:TDropDownList ID="Role">
+ &lt;com:TListItem Text="Normal User" Value="0" />
+ &lt;com:TListItem Text="Administrator" Value="1" />
+&lt;/com:TDropDownList>
+
+<br/>
+<span>First Name:</span>
+<br/>
+&lt;com:TTextBox ID="FirstName" />
+
+<br/>
+<span>Last Name:</span>
+<br/>
+&lt;com:TTextBox ID="LastName" />
+
+<br/>
+&lt;com:TButton Text="Create" OnClick="createButtonClicked" />
+
+&lt;/com:TContent>
+</com:TTextHighlighter>
+
+<p>
+The template is not much different from the <tt>Contact</tt> template and the <tt>LoginUser</tt> page. It mainly consists of text boxes and validators. Some text boxes, such as username, are associated with two validators because of the multiple validation rules involved.
+</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 two event handlers: <tt>checkUsername()</tt> (attached to the first custom validator's <tt>OnServerValidate</tt> event) and <tt>createButtonClicked()</tt> (attached to the "create" button's <tt>OnClick</tt> event). Therefore, we write the page class as follows:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="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->constructUrl($this->Service->DefaultPage));
+ }
+ }
+}
+</com:TTextHighlighter>
+
+<p>
+In the above, calling <tt>save()</tt> will insert a new row in the <tt>users</tt> table. This intuitive feature is enabled by <a href="http://www.pradosoft.com/demos/quickstart/?page=Database.ActiveRecord">Active Record</a>.
+</p>
+
+<com:NoteBox>
+For simplicity, usernames in our blog system are case-sensitive! In many practical systems, usernames may be required to be case-sensitive. So special handling needs to be made when creating a new user account as well as <a href="?page=Day3.Auth">performing authentication</a>. Also, the surrounding blanks in a username may need to be trimmed when creating a new account with it.
+</com:NoteBox>
+
+
+<h2>Testing</h2>
+<p>
+To test the <tt>NewUser</tt> page, visit the URL <tt>http://hostname/blog/index.php?page=users.NewUser</tt>. We shall see the following page output. Try enter different information into the form and see how the inputs are being validated. If all validation rules are satisfied, we shall expect the user account being created and the browser being redirected to the homepage.
+</p>
+
+<img src="<%~ output2.gif %>" class="output"/>
+
+
+<h2>Adding Permission Check</h2>
+<p>
+During testing, you may have asked: shouldn't the <tt>NewUser</tt> page be only accessible by the administrator user? Yes, this is called <a href="http://www.pradosoft.com/demos/quickstart/?page=Advanced.Auth">authorization</a>. We now describe how we add this permission check to the <tt>NewUser</tt> page.
+</p>
+
+<p>
+A straightforward way of performing permission check is in the page class where we check whether <tt>$this->User->IsAdmin</tt> is true, and if not we redirect the browser to the <tt>LoginUser</tt> page.
+</p>
+
+<p>
+PRADO offers a more systematic way of checking page access permissions. To do so, we need to use <a href="http://www.pradosoft.com/demos/quickstart/?page=Configurations.PageConfig">page configuration</a>. Create a file <tt>protected/pages/users/config.xml</tt> with the content as follows:
+</p>
+
+<com:TTextHighlighter CssClass="source" Language="xml">
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <authorization>
+ <allow pages="NewUser" roles="admin" />
+ <deny users="?" />
+ </authorization>
+</configuration>
+</com:TTextHighlighter>
+
+<p>
+The page configuration contains authorization rules that apply to the pages under the directory <tt>protected/pages/users</tt>. The above configuration reads that the <tt>NewUser</tt> can be accessed by users of role <tt>admin</tt> (see <a href="?page=Day3.Auth">BlogUser.createUser()</a> for why the word "admin"), and deny anonymous access (<tt>users="?"</tt> means guest users) for all pages under the directory.
+</p>
+
+<p>
+Now if we visit the <tt>NewUser</tt> page as a guest, we will be redirected to the <tt>LoginUser</tt> page first. If our login is successful, we will be redirected back to the <tt>NewUser</tt> page.
+</p>
+
+<com:TipBox>
+Page configuration can contain more than authorization rules. For example, it can include <a href="http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Modules">modules</a> like we did in the <a href="?page=Day2.ConnectDB">application configuration</a>. For a PRADO application, each page directory can have a page configuration which applies to the pages in the same directory and all its subdirectories.
+</com:TipBox>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog-tutorial/protected/pages/Day3/Overview.page b/demos/blog-tutorial/protected/pages/Day3/Overview.page
new file mode 100644
index 00000000..5a83e3a4
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/Overview.page
@@ -0,0 +1,26 @@
+<com:TContent ID="Main">
+
+<h1>User Management Overview</h1>
+
+<p>
+In this section, we create pages that are related with user management. In particular, we implement these required features: user login/logout, creating new user account and updating/deleting user accounts.
+</p>
+
+<p>
+According to the requirements, we need to create the following pages. To better organize our code, these user-related pages will be created under a new directory <tt>protected/pages/users</tt>.
+</p>
+
+<ul>
+ <li><tt>LoginUser</tt> displays a login form to login a user.</li>
+ <li><tt>NewUser</tt> creates a new user account.</li>
+ <li><tt>EditUser</tt> allows a registered user to update his profile.</li>
+ <li><tt>AdminUser</tt> allows the administrator to manage the user accounts, including setting permission level and deleting a user account.</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/Day3/directories.gif b/demos/blog-tutorial/protected/pages/Day3/directories.gif
new file mode 100644
index 00000000..f59fda58
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/directories.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day3/output.gif b/demos/blog-tutorial/protected/pages/Day3/output.gif
new file mode 100644
index 00000000..0d812dd0
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/output.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day3/output2.gif b/demos/blog-tutorial/protected/pages/Day3/output2.gif
new file mode 100644
index 00000000..749255d6
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/output2.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Day3/output3.gif b/demos/blog-tutorial/protected/pages/Day3/output3.gif
new file mode 100644
index 00000000..a11ee653
--- /dev/null
+++ b/demos/blog-tutorial/protected/pages/Day3/output3.gif
Binary files differ
diff --git a/demos/blog-tutorial/protected/pages/Requirements.page b/demos/blog-tutorial/protected/pages/Requirements.page
index 497eba90..3b89750c 100644
--- a/demos/blog-tutorial/protected/pages/Requirements.page
+++ b/demos/blog-tutorial/protected/pages/Requirements.page
@@ -30,7 +30,7 @@ In general, the blog system should allow users to read blogs and authenticated u
<h2>System Maintenance</h2>
<ul>
-<li>The system shall provide a mechanism to collect users' feedback.</li>
+<li>The system shall be able to collect users' feedback.</li>
<li>The system shall be flexible enough to allow adding new portlets in future.</li>
<li>The system shall provide ease of using a different theme which changes the styles of the UI elements.</li>
</ul>
diff --git a/demos/blog-tutorial/samples/day1/blog/protected/layouts/MainLayout.tpl b/demos/blog-tutorial/samples/day1/blog/protected/layouts/MainLayout.tpl
index 5218b98d..6e264936 100644
--- a/demos/blog-tutorial/samples/day1/blog/protected/layouts/MainLayout.tpl
+++ b/demos/blog-tutorial/samples/day1/blog/protected/layouts/MainLayout.tpl
@@ -12,7 +12,7 @@
</div>
<div id="footer">
-Powered by <%= PRADO::poweredByPrado() %>
+<%= PRADO::poweredByPrado() %>
</div>
</com:TForm>
diff --git a/demos/blog-tutorial/samples/day2/blog/protected/data/blog.db b/demos/blog-tutorial/samples/day2/blog/protected/data/blog.db
index 37449fd3..46e82bfb 100644
--- a/demos/blog-tutorial/samples/day2/blog/protected/data/blog.db
+++ b/demos/blog-tutorial/samples/day2/blog/protected/data/blog.db
Binary files differ
diff --git a/demos/blog-tutorial/samples/day2/blog/protected/database/PostRecord.php b/demos/blog-tutorial/samples/day2/blog/protected/database/PostRecord.php
index 31451b30..a761286a 100644
--- a/demos/blog-tutorial/samples/day2/blog/protected/database/PostRecord.php
+++ b/demos/blog-tutorial/samples/day2/blog/protected/database/PostRecord.php
@@ -1,6 +1,6 @@
<?php
/**
- * Auto generated by prado-cli.php on 2007-04-06 08:42:12.
+ * Auto generated by prado-cli.php on 2007-04-07 10:44:20.
*/
class PostRecord extends TActiveRecord
{
@@ -15,6 +15,8 @@ class PostRecord extends TActiveRecord
public $title;
public $content;
+
+ public $status;
public static function finder($className=__CLASS__)
diff --git a/demos/blog-tutorial/samples/day2/blog/protected/database/UserRecord.php b/demos/blog-tutorial/samples/day2/blog/protected/database/UserRecord.php
index 2f657cd8..87043894 100644
--- a/demos/blog-tutorial/samples/day2/blog/protected/database/UserRecord.php
+++ b/demos/blog-tutorial/samples/day2/blog/protected/database/UserRecord.php
@@ -1,6 +1,6 @@
<?php
/**
- * Auto generated by prado-cli.php on 2007-04-06 08:42:08.
+ * Auto generated by prado-cli.php on 2007-04-07 10:44:25.
*/
class UserRecord extends TActiveRecord
{
diff --git a/demos/blog-tutorial/samples/day2/blog/protected/layouts/MainLayout.tpl b/demos/blog-tutorial/samples/day2/blog/protected/layouts/MainLayout.tpl
index 5218b98d..6e264936 100644
--- a/demos/blog-tutorial/samples/day2/blog/protected/layouts/MainLayout.tpl
+++ b/demos/blog-tutorial/samples/day2/blog/protected/layouts/MainLayout.tpl
@@ -12,7 +12,7 @@
</div>
<div id="footer">
-Powered by <%= PRADO::poweredByPrado() %>
+<%= PRADO::poweredByPrado() %>
</div>
</com:TForm>
diff --git a/demos/blog-tutorial/samples/day2/blog/protected/schema.sql b/demos/blog-tutorial/samples/day2/blog/protected/schema.sql
index 085e47c3..d3189b40 100644
--- a/demos/blog-tutorial/samples/day2/blog/protected/schema.sql
+++ b/demos/blog-tutorial/samples/day2/blog/protected/schema.sql
@@ -1,8 +1,8 @@
/* create users table */
CREATE TABLE users (
username VARCHAR(128) NOT NULL PRIMARY KEY,
- email VARCHAR(128) NOT NULL UNIQUE,
- password VARCHAR(128) NOT NULL, /* plain text password */
+ 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)
@@ -14,10 +14,11 @@ CREATE TABLE posts (
author VARCHAR(128) NOT NULL, /* references users.username */
create_time INTEGER NOT NULL, /* UNIX timestamp */
title VARCHAR(256) NOT NULL, /* title of the post */
- content TEXT NOT NULL /* content of the post */
+ content TEXT NOT NULL, /* content of the post */
+ 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');
+INSERT INTO posts VALUES (NULL, 'admin', 1175708482, 'first post', 'this is my first post', 0);
diff --git a/demos/blog-tutorial/samples/day3/blog/index.php b/demos/blog-tutorial/samples/day3/blog/index.php
new file mode 100644
index 00000000..8132899e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/index.php
@@ -0,0 +1,23 @@
+<?php
+
+$frameworkPath='D:\wwwroot\prado3\framework\prado.php';
+
+// The following directory checks may be removed if performance is required
+$basePath=dirname(__FILE__);
+$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/day3/blog/protected/.htaccess b/demos/blog-tutorial/samples/day3/blog/protected/.htaccess
new file mode 100644
index 00000000..3418e55a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/.htaccess
@@ -0,0 +1 @@
+deny from all \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/BlogUser.php b/demos/blog-tutorial/samples/day3/blog/protected/BlogUser.php
new file mode 100644
index 00000000..6b9e0a23
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/application.xml b/demos/blog-tutorial/samples/day3/blog/protected/application.xml
new file mode 100644
index 00000000..785f3608
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/application.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<application id="blog" mode="Debug">
+ <paths>
+ <using namespace="Application.database.*" />
+ </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="Home">
+ <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/day3/blog/protected/data/blog.db b/demos/blog-tutorial/samples/day3/blog/protected/data/blog.db
new file mode 100644
index 00000000..37449fd3
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/data/blog.db
Binary files differ
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/database/PostRecord.php b/demos/blog-tutorial/samples/day3/blog/protected/database/PostRecord.php
new file mode 100644
index 00000000..a761286a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/database/PostRecord.php
@@ -0,0 +1,27 @@
+<?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;
+
+ public $create_time;
+
+ public $title;
+
+ public $content;
+
+ public $status;
+
+
+ public static function finder($className=__CLASS__)
+ {
+ return parent::finder($className);
+ }
+}
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/database/UserRecord.php b/demos/blog-tutorial/samples/day3/blog/protected/database/UserRecord.php
new file mode 100644
index 00000000..87043894
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/database/UserRecord.php
@@ -0,0 +1,27 @@
+<?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 static function finder($className=__CLASS__)
+ {
+ return parent::finder($className);
+ }
+}
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.php b/demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.php
new file mode 100644
index 00000000..46c1483d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/layouts/MainLayout.tpl b/demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.tpl
new file mode 100644
index 00000000..89538ab2
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/layouts/MainLayout.tpl
@@ -0,0 +1,29 @@
+<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">
+<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/day3/blog/protected/pages/Contact.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/Contact.page
new file mode 100644
index 00000000..c36149ca
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/Contact.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/Contact.php
new file mode 100644
index 00000000..b6ce575e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/Home.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/Home.page
new file mode 100644
index 00000000..7a9c4a7d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/AdminUser.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/AdminUser.page
new file mode 100644
index 00000000..af03b858
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/AdminUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/AdminUser.php
new file mode 100644
index 00000000..ad8f6e3d
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/EditUser.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.page
new file mode 100644
index 00000000..8aa3670e
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/EditUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/EditUser.php
new file mode 100644
index 00000000..cb69b9d5
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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->constructUrl($this->Service->DefaultPage));
+ }
+ }
+
+ /**
+ * 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/day3/blog/protected/pages/users/LoginUser.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.page
new file mode 100644
index 00000000..f7fc7367
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/LoginUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/LoginUser.php
new file mode 100644
index 00000000..dff8f2f0
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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->constructUrl($this->Service->DefaultPage);
+ $this->Response->redirect($url);
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.page b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.page
new file mode 100644
index 00000000..d1547a9a
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/pages/users/NewUser.php b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/NewUser.php
new file mode 100644
index 00000000..005fb7d2
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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->constructUrl($this->Service->DefaultPage));
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog-tutorial/samples/day3/blog/protected/pages/users/config.xml b/demos/blog-tutorial/samples/day3/blog/protected/pages/users/config.xml
new file mode 100644
index 00000000..56554441
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/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/day3/blog/protected/schema.sql b/demos/blog-tutorial/samples/day3/blog/protected/schema.sql
new file mode 100644
index 00000000..d3189b40
--- /dev/null
+++ b/demos/blog-tutorial/samples/day3/blog/protected/schema.sql
@@ -0,0 +1,24 @@
+/* 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 VARCHAR(128) NOT NULL, /* references users.username */
+ create_time INTEGER NOT NULL, /* UNIX timestamp */
+ title VARCHAR(256) NOT NULL, /* title of the post */
+ content TEXT NOT NULL, /* content of the post */
+ 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);
diff --git a/demos/blog-tutorial/themes/PradoSoft/style.css b/demos/blog-tutorial/themes/PradoSoft/style.css
index ddfc7622..c9391df5 100644
--- a/demos/blog-tutorial/themes/PradoSoft/style.css
+++ b/demos/blog-tutorial/themes/PradoSoft/style.css
@@ -21,14 +21,14 @@ h1, h2, h3, h4
margin-bottom: 0;
}
-h1, h2
+h1
{
padding-bottom: 3px;
border-bottom: 1px solid #ccc;
}
h1 {
- font-size:13pt;
+ font-size:14pt;
}
h2 {
@@ -657,4 +657,14 @@ div.last-modified
{
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
+}
+
+img.output
+{
+ padding: 0.5em;
+ border-style:solid;
+ border-width:1px;
+ border-color:#eeeeee;
+ background-color:#ffffee;
+ margin: 0.2em;
} \ No newline at end of file