summaryrefslogtreecommitdiff
path: root/demos/blog
diff options
context:
space:
mode:
authorxue <>2006-05-29 03:08:07 +0000
committerxue <>2006-05-29 03:08:07 +0000
commit2ea02214b2fb6bedb58dbbd318ef171a9e146524 (patch)
tree16b12d9f68986fe204900d1cee4914a0a4035a7b /demos/blog
parent8c1edb7f4eced999c9704ec9ff7ba11d88248bbd (diff)
Merge from 3.0 branch till 1099.
Diffstat (limited to 'demos/blog')
-rw-r--r--demos/blog/index.php18
-rw-r--r--demos/blog/protected/Common/BlogDataModule.php535
-rw-r--r--demos/blog/protected/Common/BlogErrors.php23
-rw-r--r--demos/blog/protected/Common/BlogException.php14
-rw-r--r--demos/blog/protected/Common/BlogPage.php26
-rw-r--r--demos/blog/protected/Common/BlogUser.php38
-rw-r--r--demos/blog/protected/Common/BlogUserManager.php56
-rw-r--r--demos/blog/protected/Common/XListMenu.php127
-rw-r--r--demos/blog/protected/Common/messages.txt4
-rw-r--r--demos/blog/protected/Common/schema.sql70
-rw-r--r--demos/blog/protected/Data/Options.xml8
-rw-r--r--demos/blog/protected/Layouts/MainLayout.php7
-rw-r--r--demos/blog/protected/Layouts/MainLayout.tpl47
-rw-r--r--demos/blog/protected/Pages/Admin/AdminMenu.php7
-rw-r--r--demos/blog/protected/Pages/Admin/AdminMenu.tpl16
-rw-r--r--demos/blog/protected/Pages/Admin/ConfigMan.page56
-rw-r--r--demos/blog/protected/Pages/Admin/ConfigMan.php15
-rw-r--r--demos/blog/protected/Pages/Admin/PostMan.page76
-rw-r--r--demos/blog/protected/Pages/Admin/PostMan.php56
-rw-r--r--demos/blog/protected/Pages/Admin/Settings.page4
-rw-r--r--demos/blog/protected/Pages/Admin/UserMan.page95
-rw-r--r--demos/blog/protected/Pages/Admin/UserMan.php58
-rw-r--r--demos/blog/protected/Pages/Admin/config.xml8
-rw-r--r--demos/blog/protected/Pages/ErrorReport.page15
-rw-r--r--demos/blog/protected/Pages/ErrorReport.php12
-rw-r--r--demos/blog/protected/Pages/Posts/EditCategory.page36
-rw-r--r--demos/blog/protected/Pages/Posts/EditCategory.php44
-rw-r--r--demos/blog/protected/Pages/Posts/EditPost.page41
-rw-r--r--demos/blog/protected/Pages/Posts/EditPost.php51
-rw-r--r--demos/blog/protected/Pages/Posts/ListPost.page27
-rw-r--r--demos/blog/protected/Pages/Posts/ListPost.php44
-rw-r--r--demos/blog/protected/Pages/Posts/MyPost.page46
-rw-r--r--demos/blog/protected/Pages/Posts/MyPost.php34
-rw-r--r--demos/blog/protected/Pages/Posts/NewCategory.page36
-rw-r--r--demos/blog/protected/Pages/Posts/NewCategory.php24
-rw-r--r--demos/blog/protected/Pages/Posts/NewPost.page41
-rw-r--r--demos/blog/protected/Pages/Posts/NewPost.php34
-rw-r--r--demos/blog/protected/Pages/Posts/ViewPost.page113
-rw-r--r--demos/blog/protected/Pages/Posts/ViewPost.php73
-rw-r--r--demos/blog/protected/Pages/Posts/config.xml7
-rw-r--r--demos/blog/protected/Pages/Users/EditUser.page74
-rw-r--r--demos/blog/protected/Pages/Users/EditUser.php43
-rw-r--r--demos/blog/protected/Pages/Users/NewUser.page104
-rw-r--r--demos/blog/protected/Pages/Users/NewUser.php31
-rw-r--r--demos/blog/protected/Pages/Users/ViewUser.page21
-rw-r--r--demos/blog/protected/Pages/Users/ViewUser.php19
-rw-r--r--demos/blog/protected/Pages/Users/config.xml7
-rw-r--r--demos/blog/protected/Portlets/AccountPortlet.php14
-rw-r--r--demos/blog/protected/Portlets/AccountPortlet.tpl20
-rw-r--r--demos/blog/protected/Portlets/ArchivePortlet.php45
-rw-r--r--demos/blog/protected/Portlets/ArchivePortlet.tpl15
-rw-r--r--demos/blog/protected/Portlets/CategoryPortlet.php15
-rw-r--r--demos/blog/protected/Portlets/CategoryPortlet.tpl24
-rw-r--r--demos/blog/protected/Portlets/LoginPortlet.php22
-rw-r--r--demos/blog/protected/Portlets/LoginPortlet.tpl36
-rw-r--r--demos/blog/protected/Portlets/Portlet.php7
-rw-r--r--demos/blog/protected/Portlets/SearchPortlet.php22
-rw-r--r--demos/blog/protected/Portlets/SearchPortlet.tpl21
-rw-r--r--demos/blog/protected/application.xml38
-rw-r--r--demos/blog/sitemap.txt106
-rw-r--r--demos/blog/themes/Basic/style.css261
61 files changed, 2987 insertions, 0 deletions
diff --git a/demos/blog/index.php b/demos/blog/index.php
new file mode 100644
index 00000000..43c0b436
--- /dev/null
+++ b/demos/blog/index.php
@@ -0,0 +1,18 @@
+<?php
+
+$basePath=dirname(__FILE__);
+$frameworkPath=$basePath.'/../../framework/prado.php';
+$assetsPath=$basePath.'/assets';
+$runtimePath=$basePath.'/protected/runtime';
+
+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/protected/Common/BlogDataModule.php b/demos/blog/protected/Common/BlogDataModule.php
new file mode 100644
index 00000000..714743e7
--- /dev/null
+++ b/demos/blog/protected/Common/BlogDataModule.php
@@ -0,0 +1,535 @@
+<?php
+
+// post status: 0 - draft, 1 - published
+// comment status: 0 - awaiting approval, 1 - published
+class BlogDataModule extends TModule
+{
+ const DB_FILE_EXT='.db';
+ const DEFAULT_DB_FILE='Application.Data.Blog';
+ private $_db=null;
+ private $_dbFile=null;
+
+ public function init($config)
+ {
+ $this->connectDatabase();
+ }
+
+ public function getDbFile()
+ {
+ if($this->_dbFile===null)
+ $this->_dbFile=Prado::getPathOfNamespace(self::DEFAULT_DB_FILE,self::DB_FILE_EXT);
+ return $this->_dbFile;
+ }
+
+ public function setDbFile($value)
+ {
+ if(($this->_dbFile=Prado::getPathOfNamespace($value,self::DB_FILE_EXT))===null)
+ throw new BlogException('blogdatamodule_dbfile_invalid',$value);
+ }
+
+ protected function createDatabase()
+ {
+ $schemaFile=dirname(__FILE__).'/schema.sql';
+ $statements=explode(';',file_get_contents($schemaFile));
+ foreach($statements as $statement)
+ {
+ if(trim($statement)!=='')
+ {
+ if(@sqlite_query($this->_db,$statement)===false)
+ throw new BlogException('blogdatamodule_createdatabase_failed',sqlite_error_string(sqlite_last_error($this->_db)),$statement);
+ }
+ }
+ }
+
+ protected function connectDatabase()
+ {
+ $dbFile=$this->getDbFile();
+ $newDb=!is_file($dbFile);
+ $error='';
+ if(($this->_db=sqlite_open($dbFile,0666,$error))===false)
+ throw new BlogException('blogdatamodule_dbconnect_failed',$error);
+ if($newDb)
+ $this->createDatabase();
+ }
+
+ protected function generateModifier($filter,$orderBy,$limit)
+ {
+ $modifier='';
+ if($filter!=='')
+ $modifier=' WHERE '.$filter;
+ if($orderBy!=='')
+ $modifier.=' ORDER BY '.$orderBy;
+ if($limit!=='')
+ $modifier.=' LIMIT '.$limit;
+ return $modifier;
+ }
+
+ public function query($sql)
+ {
+ if(($result=@sqlite_query($this->_db,$sql))!==false)
+ return $result;
+ else
+ throw new BlogException('blogdatamodule_query_failed',sqlite_error_string(sqlite_last_error($this->_db)),$sql);
+ }
+
+ protected function populateUserRecord($row)
+ {
+ $userRecord=new UserRecord;
+ $userRecord->ID=(integer)$row['id'];
+ $userRecord->Name=$row['name'];
+ $userRecord->FullName=$row['full_name'];
+ $userRecord->Role=(integer)$row['role'];
+ $userRecord->Password=$row['passwd'];
+ $userRecord->VerifyCode=$row['vcode'];
+ $userRecord->Email=$row['email'];
+ $userRecord->CreateTime=(integer)$row['reg_time'];
+ $userRecord->Status=(integer)$row['status'];
+ $userRecord->Website=$row['website'];
+ return $userRecord;
+ }
+
+ public function queryUsers($filter='',$orderBy='',$limit='')
+ {
+ if($filter!=='')
+ $filter='WHERE '.$filter;
+ $sql="SELECT * FROM tblUsers $filter $orderBy $limit";
+ $result=$this->query($sql);
+ $rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+ $users=array();
+ foreach($rows as $row)
+ $users[]=$this->populateUserRecord($row);
+ return $users;
+ }
+
+ public function queryUserCount($filter)
+ {
+ if($filter!=='')
+ $filter='WHERE '.$filter;
+ $sql="SELECT COUNT(id) AS user_count FROM tblUsers $filter";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $row['user_count'];
+ else
+ return 0;
+ }
+
+ public function queryUserByID($id)
+ {
+ $sql="SELECT * FROM tblUsers WHERE id=$id";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $this->populateUserRecord($row);
+ else
+ return null;
+ }
+
+ public function queryUserByName($name)
+ {
+ $name=sqlite_escape_string($name);
+ $sql="SELECT * FROM tblUsers WHERE name='$name'";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $this->populateUserRecord($row);
+ else
+ return null;
+ }
+
+ public function insertUser($user)
+ {
+ $name=sqlite_escape_string($user->Name);
+ $fullName=sqlite_escape_string($user->FullName);
+ $passwd=sqlite_escape_string($user->Password);
+ $email=sqlite_escape_string($user->Email);
+ $website=sqlite_escape_string($user->Website);
+ $createTime=time();
+ $sql="INSERT INTO tblUsers ".
+ "(name,full_name,role,passwd,email,reg_time,website) ".
+ "VALUES ('$name','$fullName',{$user->Role},'$passwd','$email',$createTime,'$website')";
+ $this->query($sql);
+ $user->ID=sqlite_last_insert_rowid($this->_db);
+ }
+
+ public function updateUser($user)
+ {
+ $name=sqlite_escape_string($user->Name);
+ $fullName=sqlite_escape_string($user->FullName);
+ $passwd=sqlite_escape_string($user->Password);
+ $email=sqlite_escape_string($user->Email);
+ $website=sqlite_escape_string($user->Website);
+ $sql="UPDATE tblUsers SET
+ name='$name',
+ full_name='$fullName',
+ role={$user->Role},
+ passwd='$passwd',
+ vcode='{$user->VerifyCode}',
+ email='$email',
+ status={$user->Status},
+ website='$website'
+ WHERE id={$user->ID}";
+ $this->query($sql);
+ }
+
+ public function deleteUser($id)
+ {
+ $this->query("DELETE FROM tblUsers WHERE id=$id");
+ }
+
+ protected function populatePostRecord($row)
+ {
+ $postRecord=new PostRecord;
+ $postRecord->ID=(integer)$row['id'];
+ $postRecord->AuthorID=(integer)$row['author_id'];
+ if($row['author_full_name']!=='')
+ $postRecord->AuthorName=$row['author_full_name'];
+ else
+ $postRecord->AuthorName=$row['author_name'];
+ $postRecord->CreateTime=(integer)$row['create_time'];
+ $postRecord->ModifyTime=(integer)$row['modify_time'];
+ $postRecord->Title=$row['title'];
+ $postRecord->Content=$row['content'];
+ $postRecord->Status=(integer)$row['status'];
+ $postRecord->CommentCount=(integer)$row['comment_count'];
+ return $postRecord;
+ }
+
+ public function queryPosts($authorFilter,$timeFilter,$categoryFilter,$orderBy,$limit)
+ {
+ $filter='';
+ if($authorFilter!=='')
+ $filter.=" AND $authorFilter";
+ if($timeFilter!=='')
+ $filter.=" AND $timeFilter";
+ if($categoryFilter!=='')
+ $filter.=" AND a.id IN (SELECT post_id AS id FROM tblPost2Category WHERE $categoryFilter)";
+ $sql="SELECT a.id AS id,
+ a.author_id AS author_id,
+ b.name AS author_name,
+ b.full_name AS author_full_name,
+ a.create_time AS create_time,
+ a.modify_time AS modify_time,
+ a.title AS title,
+ a.content AS content,
+ a.status AS status,
+ a.comment_count AS comment_count
+ FROM tblPosts a, tblUsers b
+ WHERE a.author_id=b.id $filter $orderBy $limit";
+ $result=$this->query($sql);
+ $rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+ $posts=array();
+ foreach($rows as $row)
+ $posts[]=$this->populatePostRecord($row);
+ return $posts;
+ }
+
+ public function queryPostCount($authorFilter,$timeFilter,$categoryFilter)
+ {
+ $filter='';
+ if($authorFilter!=='')
+ $filter.=" AND $authorFilter";
+ if($timeFilter!=='')
+ $filter.=" AND $timeFilter";
+ if($categoryFilter!=='')
+ $filter.=" AND a.id IN (SELECT post_id AS id FROM tblPost2Category WHERE $categoryFilter)";
+ $sql="SELECT COUNT(a.id) AS post_count
+ FROM tblPosts a, tblUsers b
+ WHERE a.author_id=b.id $filter";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $row['post_count'];
+ else
+ return 0;
+ }
+
+ public function queryPostByID($id)
+ {
+ $sql="SELECT a.id AS id,
+ a.author_id AS author_id,
+ b.name AS author_name,
+ b.full_name AS author_full_name,
+ a.create_time AS create_time,
+ a.modify_time AS modify_time,
+ a.title AS title,
+ a.content AS content,
+ a.status AS status,
+ a.comment_count AS comment_count
+ FROM tblPosts a, tblUsers b
+ WHERE a.id=$id AND a.author_id=b.id";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $this->populatePostRecord($row);
+ else
+ return null;
+ }
+
+ public function insertPost($post,$catIDs)
+ {
+ $title=sqlite_escape_string($post->Title);
+ $content=sqlite_escape_string($post->Content);
+ $sql="INSERT INTO tblPosts
+ (author_id,create_time,title,content,status)
+ VALUES ({$post->AuthorID},{$post->CreateTime},'$title','$content',{$post->Status})";
+ $this->query($sql);
+ $post->ID=sqlite_last_insert_rowid($this->_db);
+ foreach($catIDs as $catID)
+ $this->insertPostCategory($post->ID,$catID);
+ }
+
+ public function updatePost($post,$newCatIDs=null)
+ {
+ if($newCatIDs!==null)
+ {
+ $cats=$this->queryCategoriesByPostID($post->ID);
+ $catIDs=array();
+ foreach($cats as $cat)
+ $catIDs[]=$cat->ID;
+ $deleteIDs=array_diff($catIDs,$newCatIDs);
+ foreach($deleteIDs as $id)
+ $this->deletePostCategory($post->ID,$id);
+ $insertIDs=array_diff($newCatIDs,$catIDs);
+ foreach($insertIDs as $id)
+ $this->insertPostCategory($post->ID,$id);
+ }
+
+ $title=sqlite_escape_string($post->Title);
+ $content=sqlite_escape_string($post->Content);
+ $sql="UPDATE tblPosts SET
+ modify_time={$post->ModifyTime},
+ title='$title',
+ content='$content',
+ status={$post->Status}
+ WHERE id={$post->ID}";
+ $this->query($sql);
+ }
+
+ public function deletePost($id)
+ {
+ $cats=$this->queryCategoriesByPostID($id);
+ foreach($cats as $cat)
+ $this->deletePostCategory($id,$cat->ID);
+ $this->query("DELETE FROM tblComments WHERE post_id=$id");
+ $this->query("DELETE FROM tblPosts WHERE id=$id");
+ }
+
+ protected function populateCommentRecord($row)
+ {
+ $commentRecord=new CommentRecord;
+ $commentRecord->ID=(integer)$row['id'];
+ $commentRecord->PostID=(integer)$row['post_id'];
+ $commentRecord->AuthorName=$row['author_name'];
+ $commentRecord->AuthorEmail=$row['author_email'];
+ $commentRecord->AuthorWebsite=$row['author_website'];
+ $commentRecord->AuthorIP=$row['author_ip'];
+ $commentRecord->CreateTime=(integer)$row['create_time'];
+ $commentRecord->Content=$row['content'];
+ $commentRecord->Status=(integer)$row['status'];
+ return $commentRecord;
+ }
+
+ public function queryCommentsByPostID($id)
+ {
+ $sql="SELECT * FROM tblComments WHERE post_id=$id";
+ $result=$this->query($sql);
+ $rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+ $comments=array();
+ foreach($rows as $row)
+ $comments[]=$this->populateCommentRecord($row);
+ return $comments;
+ }
+
+ public function insertComment($comment)
+ {
+ $authorName=sqlite_escape_string($comment->AuthorName);
+ $authorEmail=sqlite_escape_string($comment->AuthorEmail);
+ $authorWebsite=sqlite_escape_string($comment->AuthorWebsite);
+ $content=sqlite_escape_string($comment->Content);
+ $sql="INSERT INTO tblComments
+ (post_id,author_name,author_email,author_website,author_ip,create_time,status,content)
+ VALUES ({$comment->PostID},'$authorName','$authorEmail','$authorWebsite','{$comment->AuthorIP}',{$comment->CreateTime},{$comment->Status},'$content')";
+ $this->query($sql);
+ $comment->ID=sqlite_last_insert_rowid($this->_db);
+ $this->query("UPDATE tblPosts SET comment_count=comment_count+1 WHERE id={$comment->PostID}");
+ }
+
+ public function updateComment($comment)
+ {
+ $authorName=sqlite_escape_string($comment->AuthorName);
+ $authorEmail=sqlite_escape_string($comment->AuthorEmail);
+ $content=sqlite_escape_string($comment->Content);
+ $sql="UPDATE tblComments SET status={$comment->Status} WHERE id={$comment->ID}";
+ $this->query($sql);
+ }
+
+ public function deleteComment($id)
+ {
+ $result=$this->query("SELECT post_id FROM tblComments WHERE id=$id");
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ {
+ $postID=$row['post_id'];
+ $this->query("DELETE FROM tblComments WHERE id=$id");
+ $this->query("UPDATE tblPosts SET comment_count=comment_count-1 WHERE id=$postID");
+ }
+ }
+
+ protected function populateCategoryRecord($row)
+ {
+ $catRecord=new CategoryRecord;
+ $catRecord->ID=(integer)$row['id'];
+ $catRecord->Name=$row['name'];
+ $catRecord->Description=$row['description'];
+ $catRecord->PostCount=$row['post_count'];
+ return $catRecord;
+ }
+
+ public function queryCategories()
+ {
+ $sql="SELECT * FROM tblCategories";
+ $result=$this->query($sql);
+ $rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+ $cats=array();
+ foreach($rows as $row)
+ $cats[]=$this->populateCategoryRecord($row);
+ return $cats;
+ }
+
+ public function queryCategoriesByPostID($postID)
+ {
+ $sql="SELECT a.id AS id,
+ a.name AS name,
+ a.description AS description,
+ a.post_count AS post_count
+ FROM tblCategories a, tblPost2Category b
+ WHERE a.id=b.category_id AND b.post_id=$postID";
+ $result=$this->query($sql);
+ $rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+ $cats=array();
+ foreach($rows as $row)
+ $cats[]=$this->populateCategoryRecord($row);
+ return $cats;
+ }
+
+ public function queryCategoryByID($id)
+ {
+ $sql="SELECT * FROM tblCategories WHERE id=$id";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $this->populateCategoryRecord($row);
+ else
+ return null;
+ }
+
+ public function queryCategoryByName($name)
+ {
+ $name=sqlite_escape_string($name);
+ $sql="SELECT * FROM tblCategories WHERE name='$name'";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $this->populateCategoryRecord($row);
+ else
+ return null;
+ }
+
+ public function insertCategory($category)
+ {
+ $name=sqlite_escape_string($category->Name);
+ $description=sqlite_escape_string($category->Description);
+ $sql="INSERT INTO tblCategories
+ (name,description)
+ VALUES ('$name','$description')";
+ $this->query($sql);
+ $category->ID=sqlite_last_insert_rowid($this->_db);
+ }
+
+ public function updateCategory($category)
+ {
+ $name=sqlite_escape_string($category->Name);
+ $description=sqlite_escape_string($category->Description);
+ $sql="UPDATE tblCategories SET name='$name', description='$description', post_count={$category->PostCount} WHERE id={$category->ID}";
+ $this->query($sql);
+ }
+
+ public function deleteCategory($id)
+ {
+ $sql="DELETE FROM tblPost2Category WHERE category_id=$id";
+ $this->query($sql);
+ $sql="DELETE FROM tblCategories WHERE id=$id";
+ $this->query($sql);
+ }
+
+ public function insertPostCategory($postID,$categoryID)
+ {
+ $sql="INSERT INTO tblPost2Category (post_id, category_id) VALUES ($postID, $categoryID)";
+ $this->query($sql);
+ $sql="UPDATE tblCategories SET post_count=post_count+1 WHERE id=$categoryID";
+ $this->query($sql);
+ }
+
+ public function deletePostCategory($postID,$categoryID)
+ {
+ $sql="DELETE FROM tblPost2Category WHERE post_id=$postID AND category_id=$categoryID";
+ if($this->query($sql)>0)
+ {
+ $sql="UPDATE tblCategories SET post_count=post_count-1 WHERE id=$categoryID";
+ $this->query($sql);
+ }
+ }
+
+ public function queryEarliestPostTime()
+ {
+ $sql="SELECT MIN(create_time) AS create_time FROM tblPosts";
+ $result=$this->query($sql);
+ if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+ return $row['create_time'];
+ else
+ return time();
+ }
+}
+
+class UserRecord
+{
+ public $ID;
+ public $Name;
+ public $FullName;
+ public $Role;
+ public $Password;
+ public $VerifyCode;
+ public $Email;
+ public $CreateTime;
+ public $Status;
+ public $Website;
+}
+
+class PostRecord
+{
+ public $ID;
+ public $AuthorID;
+ public $AuthorName;
+ public $CreateTime;
+ public $ModifyTime;
+ public $Title;
+ public $Content;
+ public $Status;
+ public $CommentCount;
+}
+
+class CommentRecord
+{
+ public $ID;
+ public $PostID;
+ public $AuthorName;
+ public $AuthorEmail;
+ public $AuthorWebsite;
+ public $AuthorIP;
+ public $CreateTime;
+ public $Status;
+ public $Content;
+}
+
+class CategoryRecord
+{
+ public $ID;
+ public $Name;
+ public $Description;
+ public $PostCount;
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogErrors.php b/demos/blog/protected/Common/BlogErrors.php
new file mode 100644
index 00000000..501ec1c9
--- /dev/null
+++ b/demos/blog/protected/Common/BlogErrors.php
@@ -0,0 +1,23 @@
+<?php
+
+class BlogErrors
+{
+ const ERROR_UKNOWN=0;
+ const ERROR_POST_NOT_FOUND=1;
+ const ERROR_USER_NOT_FOUND=2;
+ const ERROR_PERMISSION_DENIED=3;
+
+ private static $_errorMessages=array(
+ self::ERROR_UKNOWN=>'Unknown error.',
+ self::ERROR_POST_NOT_FOUND=>'The specified post cannot be found.',
+ self::ERROR_USER_NOT_FOUND=>'The specified user account cannot be found.',
+ self::ERROR_PERMISSION_DENIED=>'Sorry, you do not have permission to perform this action.',
+ );
+
+ public static function getMessage($errorCode)
+ {
+ return isset(self::$_errorMessages[$errorCode])?self::$_errorMessages[$errorCode]:self::$_errorMessages[0];
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogException.php b/demos/blog/protected/Common/BlogException.php
new file mode 100644
index 00000000..ab8020d1
--- /dev/null
+++ b/demos/blog/protected/Common/BlogException.php
@@ -0,0 +1,14 @@
+<?php
+
+class BlogException extends TApplicationException
+{
+ /**
+ * @return string path to the error message file
+ */
+ protected function getErrorMessageFile()
+ {
+ return dirname(__FILE__).'/messages.txt';
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogPage.php b/demos/blog/protected/Common/BlogPage.php
new file mode 100644
index 00000000..f1634a80
--- /dev/null
+++ b/demos/blog/protected/Common/BlogPage.php
@@ -0,0 +1,26 @@
+<?php
+
+class BlogPage extends TPage
+{
+ public function getDataAccess()
+ {
+ return $this->getApplication()->getModule('data');
+ }
+
+ public function gotoDefaultPage()
+ {
+ $this->Response->redirect($this->Service->constructUrl($this->Service->DefaultPage));
+ }
+
+ public function gotoPage($pagePath,$getParameters=null)
+ {
+ $this->Response->redirect($this->Service->constructUrl($pagePath,$getParameters));
+ }
+
+ public function reportError($errorCode)
+ {
+ $this->gotoPage('ErrorReport',array('id'=>$errorCode));
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogUser.php b/demos/blog/protected/Common/BlogUser.php
new file mode 100644
index 00000000..af49c8d7
--- /dev/null
+++ b/demos/blog/protected/Common/BlogUser.php
@@ -0,0 +1,38 @@
+<?php
+
+Prado::using('System.Security.TUser');
+
+class BlogUser extends TUser
+{
+ private $_id;
+
+ public function getID()
+ {
+ return $this->_id;
+ }
+
+ public function setID($value)
+ {
+ $this->_id=$value;
+ }
+
+ public function saveToString()
+ {
+ $a=array($this->_id,parent::saveToString());
+ return serialize($a);
+ }
+
+ public function loadFromString($data)
+ {
+ if(!empty($data))
+ {
+ list($id,$str)=unserialize($data);
+ $this->_id=$id;
+ return parent::loadFromString($str);
+ }
+ else
+ return $this;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogUserManager.php b/demos/blog/protected/Common/BlogUserManager.php
new file mode 100644
index 00000000..c3ddb80b
--- /dev/null
+++ b/demos/blog/protected/Common/BlogUserManager.php
@@ -0,0 +1,56 @@
+<?php
+
+Prado::using('System.Security.IUserManager');
+Prado::using('Application.Common.BlogUser');
+
+class BlogUserManager extends TModule implements IUserManager
+{
+ public function getGuestName()
+ {
+ return 'Guest';
+ }
+
+ /**
+ * Returns a user instance given the user name.
+ * @param string user name, null if it is a guest.
+ * @return TUser the user instance, null if the specified username is not in the user database.
+ */
+ public function getUser($username=null)
+ {
+ if($username===null)
+ return new BlogUser($this);
+ else
+ {
+ $username=strtolower($username);
+ $db=$this->Application->getModule('data');
+ if(($userRecord=$db->queryUserByName($username))!==null)
+ {
+ $user=new BlogUser($this);
+ $user->setID($userRecord->ID);
+ $user->setName($username);
+ $user->setIsGuest(false);
+ $user->setRoles($userRecord->Role===0?'user':'admin');
+ return $user;
+ }
+ else
+ return null;
+ }
+ }
+
+ /**
+ * Validates if the username and password are correct.
+ * @param string user name
+ * @param string password
+ * @return boolean true if validation is successful, false otherwise.
+ */
+ public function validateUser($username,$password)
+ {
+ $db=$this->Application->getModule('data');
+ if(($userRecord=$db->queryUserByName($username))!==null)
+ return $userRecord->Password===md5($password);
+ else
+ return false;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/XListMenu.php b/demos/blog/protected/Common/XListMenu.php
new file mode 100644
index 00000000..f8223585
--- /dev/null
+++ b/demos/blog/protected/Common/XListMenu.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * XListMenu and XListMenuItem class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2006 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ * @version $Revision: $ $Date: $
+ */
+
+Prado::using('System.Web.UI.WebControls.TListControl');
+
+/**
+ * XListMenu class
+ *
+ * XListMenu displays a list of hyperlinks that can be used for page menus.
+ * Menu items adjust their css class automatically according to the current
+ * page displayed. In particular, a menu item is considered as active if
+ * the URL it represents is for the page currently displayed.
+ *
+ * Usage of XListMenu is similar to PRADO list controls. Each list item has
+ * two extra properties: {@link XListMenuItem::setPagePath PagePath} and
+ * {@link XListMenuItem::setNavigateUrl NavigateUrl}. The former is used to
+ * determine if the item is active or not, while the latter specifies the
+ * URL for the item. If the latter is not specified, a URL to the page is
+ * generated automatically.
+ *
+ * In template, you may use the following tags to specify a menu:
+ * <code>
+ * <com:XListMenu ActiveCssClass="class1" InactiveCssClass="class2">
+ * <com:XListMenuItem Text="Menu 1" PagePath="Page1" />
+ * <com:XListMenuItem Text="Menu 2" PagePath="Page2" NavigateUrl="/page2" />
+ * </com:XListMenu>
+ * </code>
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2006 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ */
+class XListMenu extends TListControl
+{
+ public function addParsedObject($object)
+ {
+ if($object instanceof XListMenuItem)
+ parent::addParsedObject($object);
+ }
+
+ public function getActiveCssClass()
+ {
+ return $this->getViewState('ActiveCssClass','');
+ }
+
+ public function setActiveCssClass($value)
+ {
+ $this->setViewState('ActiveCssClass',$value,'');
+ }
+
+ public function getInactiveCssClass()
+ {
+ return $this->getViewState('InactiveCssClass','');
+ }
+
+ public function setInactiveCssClass($value)
+ {
+ $this->setViewState('InactiveCssClass',$value,'');
+ }
+
+ public function render($writer)
+ {
+ if(($activeClass=$this->getActiveCssClass())!=='')
+ $activeClass=' class="'.$activeClass.'"';
+ if(($inactiveClass=$this->getInactiveCssClass())!=='')
+ $inactiveClass=' class="'.$inactiveClass.'"';
+ $currentPagePath=$this->getPage()->getPagePath();
+ $writer->write("<ul>\n");
+ foreach($this->getItems() as $item)
+ {
+ $pagePath=$item->getPagePath();
+ //if(strpos($currentPagePath.'.',$pagePath.'.')===0)
+ if($pagePath[strlen($pagePath)-1]==='*')
+ {
+ if(strpos($currentPagePath.'.',rtrim($pagePath,'*'))===0)
+ $cssClass=$activeClass;
+ else
+ $cssClass=$inactiveClass;
+ }
+ else
+ {
+ if($pagePath===$currentPagePath)
+ $cssClass=$activeClass;
+ else
+ $cssClass=$inactiveClass;
+ }
+ if(($url=$item->getNavigateUrl())==='')
+ $url=$this->getService()->constructUrl($pagePath);
+ $writer->write("<li><a href=\"$url\"$cssClass>".$item->getText()."</a></li>\n");
+ }
+ $writer->write("</ul>");
+ }
+}
+
+class XListMenuItem extends TListItem
+{
+ public function getPagePath()
+ {
+ return $this->getValue();
+ }
+
+ public function setPagePath($value)
+ {
+ $this->setValue($value);
+ }
+
+ public function getNavigateUrl()
+ {
+ return $this->hasAttribute('NavigateUrl')?$this->getAttribute('NavigateUrl'):'';
+ }
+
+ public function setNavigateUrl($value)
+ {
+ $this->setAttribute('NavigateUrl',$value);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Common/messages.txt b/demos/blog/protected/Common/messages.txt
new file mode 100644
index 00000000..deb15ee3
--- /dev/null
+++ b/demos/blog/protected/Common/messages.txt
@@ -0,0 +1,4 @@
+blogdatamodule_dbconnect_failed = Unable to connect to database: {0}
+blogdatamodule_dbfile_invalid = BlogDataModule.DbFile='{0}' is invalid.
+blogdatamodule_createdatabase_failed = BlogDataModule failed to create database when executing SQL: {1}. Last SQL error is: {0}.
+blogdatamodule_query_failed = Failed to execute SQL: {1}. Last SQL error is: {0}. \ No newline at end of file
diff --git a/demos/blog/protected/Common/schema.sql b/demos/blog/protected/Common/schema.sql
new file mode 100644
index 00000000..49f6f429
--- /dev/null
+++ b/demos/blog/protected/Common/schema.sql
@@ -0,0 +1,70 @@
+CREATE TABLE tblUsers (
+ id INTEGER NOT NULL PRIMARY KEY,
+ name VARCHAR(128) NOT NULL UNIQUE,
+ full_name VARCHAR(128) DEFAULT '',
+ role INTEGER NOT NULL DEFAULT 0, /* 0: user, 1: admin */
+ passwd VARCHAR(128) NOT NULL,
+ vcode VARCHAR(128) DEFAULT '',
+ email VARCHAR(128) NOT NULL,
+ reg_time INTEGER NOT NULL,
+ status INTEGER NOT NULL DEFAULT 0, /* 0: normal, 1: disabled, 2: pending approval */
+ website VARCHAR(128) DEFAULT ''
+);
+
+CREATE TABLE tblPosts (
+ id INTEGER NOT NULL PRIMARY KEY,
+ author_id INTEGER NOT NULL,
+ create_time INTEGER NOT NULL,
+ modify_time INTEGER DEFAULT 0,
+ title VARCHAR(256) NOT NULL,
+ content TEXT NOT NULL,
+ status INTEGER NOT NULL DEFAULT 0, /* 0: published, 1: draft, 2: pending approval */
+ comment_count INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblComments (
+ id INTEGER NOT NULL PRIMARY KEY,
+ post_id INTEGER NOT NULL,
+ author_name VARCHAR(64) NOT NULL,
+ author_email VARCHAR(128) NOT NULL,
+ author_website VARCHAR(128) DEFAULT '',
+ author_ip CHAR(16) NOT NULL,
+ create_time INTEGER NOT NULL,
+ status INTEGER NOT NULL DEFAULT 0, /* 0: published, 1: pending approval */
+ content TEXT NOT NULL
+);
+
+CREATE TABLE tblCategories (
+ id INTEGER NOT NULL PRIMARY KEY,
+ name VARCHAR(128) NOT NULL UNIQUE,
+ description TEXT DEFAULT '',
+ post_count INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblAttachments (
+ id VARCHAR(128) NOT NULL PRIMARY KEY,
+ post_id INTEGER NOT NULL,
+ create_time INTEGER NOT NULL,
+ file_name VARCHAR(128) NOT NULL,
+ file_size INTEGER NOT NULL,
+ mime_type VARCHAR(32) NOT NULL DEFAULT 'text/html',
+ download_count INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblPost2Category (
+ post_id INTEGER NOT NULL,
+ category_id INTEGER NOT NULL,
+ PRIMARY KEY (post_id, category_id)
+);
+
+INSERT INTO tblUsers (id,name,full_name,role,status,passwd,email,reg_time,website)
+ VALUES (1,'admin','Prado User',1,0,'4d688da592969d0a56b5accec3ce8554','admin@example.com',1148819681,'http://www.pradosoft.com');
+
+INSERT INTO tblPosts (id,author_id,create_time,title,content,status)
+ VALUES (1,1,1148819691,'Welcome to Prado Weblog','Congratulations! You have successfully installed Prado Weblog. An administrator account has been created. Please login with <b>admin/prado</b> and update your password as soon as possible.',0);
+
+INSERT INTO tblCategories (name,description,post_count)
+ VALUES ('Miscellaneous','This category holds posts on any topic.',1);
+
+INSERT INTO tblPost2Category (post_id,category_id)
+ VALUES (1,1);
diff --git a/demos/blog/protected/Data/Options.xml b/demos/blog/protected/Data/Options.xml
new file mode 100644
index 00000000..02e51a98
--- /dev/null
+++ b/demos/blog/protected/Data/Options.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<parameters>
+ <parameter id="SiteTitle" value="Qiang's Blog" />
+ <parameter id="SiteSubtitle" value="A PRADO-driven weblog" />
+ <parameter id="SiteOwner" value="Qiang Xue" />
+ <parameter id="AdminEmail" value="admin@example.com" />
+</parameters> \ No newline at end of file
diff --git a/demos/blog/protected/Layouts/MainLayout.php b/demos/blog/protected/Layouts/MainLayout.php
new file mode 100644
index 00000000..253d6c03
--- /dev/null
+++ b/demos/blog/protected/Layouts/MainLayout.php
@@ -0,0 +1,7 @@
+<?php
+
+class MainLayout extends TTemplateControl
+{
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Layouts/MainLayout.tpl b/demos/blog/protected/Layouts/MainLayout.tpl
new file mode 100644
index 00000000..f171de7f
--- /dev/null
+++ b/demos/blog/protected/Layouts/MainLayout.tpl
@@ -0,0 +1,47 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
+
+<com:THead Title=<%$ SiteName %> >
+<meta http-equiv="Expires" content="Fri, Jan 01 1900 00:00:00 GMT"/>
+<meta http-equiv="Pragma" content="no-cache"/>
+<meta http-equiv="Cache-Control" content="no-cache"/>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<meta http-equiv="content-language" content="en"/>
+</com:THead>
+
+<body>
+<div id="page">
+<com:TForm>
+
+<div id="header">
+<h1 id="header-title"><%$ SiteTitle %></h1>
+<h2 id="header-subtitle"><%$ SiteSubtitle %></h2>
+</div><!-- end of header -->
+
+<div id="main">
+<com:TContentPlaceHolder ID="Main" />
+</div><!-- end of main -->
+
+<div id="sidebar">
+
+<com:Application.Portlets.LoginPortlet Visible=<%= $this->User->IsGuest %>/>
+
+<com:Application.Portlets.AccountPortlet Visible=<%= !$this->User->IsGuest %>/>
+
+<com:Application.Portlets.SearchPortlet />
+
+<com:Application.Portlets.CategoryPortlet />
+
+<com:Application.Portlets.ArchivePortlet />
+
+</div><!-- end of sidebar -->
+
+<div id="footer">
+Copyright &copy; 2006 <%$ SiteOwner %>.<br/>
+<%= Prado::poweredByPrado() %>
+</div><!-- end of footer -->
+
+</com:TForm>
+</div><!-- end of page -->
+</body>
+</html> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/AdminMenu.php b/demos/blog/protected/Pages/Admin/AdminMenu.php
new file mode 100644
index 00000000..40f40b88
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/AdminMenu.php
@@ -0,0 +1,7 @@
+<?php
+
+class AdminMenu extends TTemplateControl
+{
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/AdminMenu.tpl b/demos/blog/protected/Pages/Admin/AdminMenu.tpl
new file mode 100644
index 00000000..596f3ed2
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/AdminMenu.tpl
@@ -0,0 +1,16 @@
+<div class="submenu">
+<com:XListMenu ActiveCssClass="submenu-active" InactiveCssClass="submenu-inactive">
+ <com:XListMenuItem
+ Text="Posts"
+ PagePath="Admin.PostMan"
+ NavigateUrl=<%= $this->Service->constructUrl('Admin.PostMan') %> />
+ <com:XListMenuItem
+ Text="Users"
+ PagePath="Admin.UserMan"
+ NavigateUrl=<%= $this->Service->constructUrl('Admin.UserMan') %> />
+ <com:XListMenuItem
+ Text="Configurations"
+ PagePath="Admin.ConfigMan"
+ NavigateUrl=<%= $this->Service->constructUrl('Admin.ConfigMan') %> />
+</com:XListMenu>
+</div> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/ConfigMan.page b/demos/blog/protected/Pages/Admin/ConfigMan.page
new file mode 100644
index 00000000..ad728284
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/ConfigMan.page
@@ -0,0 +1,56 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TPanel GroupingText="Site settings">
+
+<span class="input-label">Title</span>
+<br/>
+<com:TTextBox ID="SiteTitle" />
+<br/>
+
+<span class="input-label">Subtitle</span>
+<br/>
+<com:TTextBox ID="SiteSubtitle" />
+<br/>
+
+<span class="input-label">Owner name</span>
+<br/>
+<com:TTextBox ID="SiteOwner" />
+<br/>
+
+<span class="input-label">Owner email</span>
+<br/>
+<com:TTextBox ID="AdminEmail" />
+<br/>
+
+<span class="input-label">Site theme</span>
+<br/>
+<com:TDropDownList ID="ThemeList" />
+<br/>
+
+</com:TPanel>
+
+
+<com:TPanel GroupingText="Account settings">
+
+<com:TCheckBox ID="MultipleUser" Text="Allow multiple users" />
+<br/>
+
+<com:TCheckBox ID="AccountApproval" Text="New accounts need approval" />
+<br/>
+
+</com:TPanel>
+
+<com:TPanel GroupingText="Post settings">
+
+<com:TCheckBox ID="PostApproval" Text="New posts need approval" />
+<br/>
+
+</com:TPanel>
+
+<com:TLinkButton Text="Save" OnClick="saveButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/ConfigMan.php b/demos/blog/protected/Pages/Admin/ConfigMan.php
new file mode 100644
index 00000000..dcbe1537
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/ConfigMan.php
@@ -0,0 +1,15 @@
+<?php
+
+class ConfigMan extends BlogPage
+{
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ }
+
+ public function saveButtonClicked($sender,$param)
+ {
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/PostMan.page b/demos/blog/protected/Pages/Admin/PostMan.page
new file mode 100644
index 00000000..8ba8ef29
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/PostMan.page
@@ -0,0 +1,76 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TDataGrid ID="PostGrid"
+ AutoGenerateColumns="false"
+ DataKeyField="ID"
+ CssClass="grid"
+ HeaderStyle.CssClass="grid-header"
+ ItemStyle.CssClass="grid-row1"
+ SelectedItemStyle.CssClass="grid-row-selected"
+ AlternatingItemStyle.CssClass="grid-row2"
+ AllowPaging="true"
+ AllowCustomPaging="true"
+ PageSize="20"
+ PagerStyle.CssClass="grid-pager"
+ PagerStyle.Mode="Numeric"
+ OnPageIndexChanged="changePage"
+ OnPagerCreated="pagerCreated"
+ OnEditCommand="editItem"
+ OnUpdateCommand="saveItem"
+ OnCancelCommand="cancelItem"
+ >
+ <com:THyperLinkColumn
+ HeaderText="Title"
+ DataNavigateUrlField="ID"
+ DataNavigateUrlFormatString="#$this->Service->constructUrl('Posts.ViewPost',array('id'=>{0}))"
+ DataTextField="Title"
+ />
+ <com:THyperLinkColumn
+ HeaderText="Author"
+ DataNavigateUrlField="AuthorID"
+ DataNavigateUrlFormatString="#$this->Service->constructUrl('Users.ViewUser',array('id'=>{0}))"
+ DataTextField="AuthorName"
+ />
+ <com:TTemplateColumn
+ HeaderText="Status"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Width="90px" >
+ <prop:ItemTemplate>
+ <%#
+ $this->Parent->DataItem->Status===0 ?
+ 'Published' :
+ ($this->Parent->DataItem->Status===1 ? 'Draft' : 'Pending')
+ %>
+ </prop:ItemTemplate>
+ <prop:EditItemTemplate>
+ <com:TDropDownList ID="PostStatus" SelectedValue=<%# $this->Parent->DataItem->Status %> >
+ <com:TListItem Value="0" Text="Published" />
+ <com:TListItem Value="1" Text="Draft" />
+ <com:TListItem Value="2" Text="Pending" />
+ </com:TDropDownList>
+ </prop:EditItemTemplate>
+ </com:TTemplateColumn>
+ <com:TBoundColumn
+ HeaderText="Time"
+ ReadOnly="true"
+ DataField="CreateTime"
+ DataFormatString="#date('M j, Y',{0})"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="90px"
+ ItemStyle.HorizontalAlign="Center"
+ />
+ <com:TEditCommandColumn
+ HeaderText="Command"
+ HeaderStyle.Width="80px"
+ UpdateText="Save"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="80px"
+ />
+</com:TDataGrid>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/PostMan.php b/demos/blog/protected/Pages/Admin/PostMan.php
new file mode 100644
index 00000000..a99332eb
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/PostMan.php
@@ -0,0 +1,56 @@
+<?php
+
+class PostMan extends BlogPage
+{
+ protected function bindData()
+ {
+ $offset=$this->PostGrid->CurrentPageIndex*$this->PostGrid->PageSize;
+ $limit=$this->PostGrid->PageSize;
+ $this->PostGrid->DataSource=$this->DataAccess->queryPosts('','','','ORDER BY a.status DESC, create_time DESC',"LIMIT $offset,$limit");
+ $this->PostGrid->VirtualItemCount=$this->DataAccess->queryPostCount('','','');
+ $this->PostGrid->dataBind();
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ $this->bindData();
+ }
+
+ public function changePage($sender,$param)
+ {
+ $this->PostGrid->CurrentPageIndex=$param->NewPageIndex;
+ $this->bindData();
+ }
+
+ public function pagerCreated($sender,$param)
+ {
+ $param->Pager->Controls->insertAt(0,'Page: ');
+ }
+
+ public function editItem($sender,$param)
+ {
+ $this->PostGrid->EditItemIndex=$param->Item->ItemIndex;
+ $this->bindData();
+ }
+
+ public function saveItem($sender,$param)
+ {
+ $item=$param->Item;
+ $postID=$this->PostGrid->DataKeys[$item->ItemIndex];
+ $postRecord=$this->DataAccess->queryPostByID($postID);
+ $postRecord->Status=TPropertyValue::ensureInteger($item->Cells[2]->PostStatus->SelectedValue);
+ $this->DataAccess->updatePost($postRecord);
+ $this->PostGrid->EditItemIndex=-1;
+ $this->bindData();
+ }
+
+ public function cancelItem($sender,$param)
+ {
+ $this->PostGrid->EditItemIndex=-1;
+ $this->bindData();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/Settings.page b/demos/blog/protected/Pages/Admin/Settings.page
new file mode 100644
index 00000000..48dfde96
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/Settings.page
@@ -0,0 +1,4 @@
+<com:TContent ID="main" >
+Welcome, <com:TLabel Text=<%= $this->User->Name %> />!
+This page contains site settings accessible only to site admin.
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/UserMan.page b/demos/blog/protected/Pages/Admin/UserMan.page
new file mode 100644
index 00000000..a8b634c6
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/UserMan.page
@@ -0,0 +1,95 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TDataGrid ID="UserGrid"
+ AutoGenerateColumns="false"
+ DataKeyField="ID"
+ CssClass="grid"
+ HeaderStyle.CssClass="grid-header"
+ ItemStyle.CssClass="grid-row1"
+ SelectedItemStyle.CssClass="grid-row-selected"
+ AlternatingItemStyle.CssClass="grid-row2"
+ AllowPaging="true"
+ AllowCustomPaging="true"
+ PageSize="20"
+ PagerStyle.CssClass="grid-pager"
+ PagerStyle.Mode="Numeric"
+ OnPageIndexChanged="changePage"
+ OnPagerCreated="pagerCreated"
+ OnEditCommand="editItem"
+ OnUpdateCommand="saveItem"
+ OnCancelCommand="cancelItem"
+ >
+ <com:THyperLinkColumn
+ HeaderText="Name"
+ DataNavigateUrlField="ID"
+ DataNavigateUrlFormatString="#$this->Service->constructUrl('Users.ViewUser',array('id'=>{0}))"
+ DataTextField="Name"
+ />
+ <com:TTemplateColumn
+ HeaderText="Role"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="7px" >
+ <prop:ItemTemplate>
+ <%# $this->Parent->DataItem->Role===0 ? 'User' : 'Admin' %>
+ </prop:ItemTemplate>
+ <prop:EditItemTemplate>
+ <com:TDropDownList ID="UserRole" SelectedValue=<%# $this->Parent->DataItem->Role %> >
+ <com:TListItem Value="0" Text="User" />
+ <com:TListItem Value="1" Text="Admin" />
+ </com:TDropDownList>
+ </prop:EditItemTemplate>
+ </com:TTemplateColumn>
+ <com:TTemplateColumn
+ HeaderText="Status"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="70px" >
+ <prop:ItemTemplate>
+ <%#
+ $this->Parent->DataItem->Status===0 ?
+ 'Normal' :
+ ($this->Parent->DataItem->Status===1 ? 'Disabled' : 'Pending')
+ %>
+ </prop:ItemTemplate>
+ <prop:EditItemTemplate>
+ <com:TDropDownList ID="UserStatus" SelectedValue=<%# $this->Parent->DataItem->Status %> >
+ <com:TListItem Value="0" Text="Normal" />
+ <com:TListItem Value="1" Text="Disabled" />
+ <com:TListItem Value="2" Text="Pending" />
+ </com:TDropDownList>
+ </prop:EditItemTemplate>
+ </com:TTemplateColumn>
+ <com:TBoundColumn
+ HeaderText="Email"
+ DataField="Email"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="90px"
+ ReadOnly="true" >
+ <prop:DataFormatString>#
+ '<a href="mailto:'.{0}.'">'.{0}.'</a>'
+ </prop:DataFormatString>
+ </com:TBoundColumn>
+ <com:TBoundColumn
+ HeaderText="Reg. Date"
+ DataField="CreateTime"
+ DataFormatString="#date('M j, Y',{0})"
+ ReadOnly="true"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="90px"
+ />
+ <com:TEditCommandColumn
+ HeaderText="Command"
+ HeaderStyle.Width="80px"
+ UpdateText="Save"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="80px"
+ />
+</com:TDataGrid>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/UserMan.php b/demos/blog/protected/Pages/Admin/UserMan.php
new file mode 100644
index 00000000..1cb62482
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/UserMan.php
@@ -0,0 +1,58 @@
+<?php
+
+class UserMan extends BlogPage
+{
+ protected function bindData()
+ {
+ $author=$this->User->ID;
+ $offset=$this->UserGrid->CurrentPageIndex*$this->UserGrid->PageSize;
+ $limit=$this->UserGrid->PageSize;
+ $this->UserGrid->DataSource=$this->DataAccess->queryUsers('','ORDER BY status DESC, name ASC',"LIMIT $offset,$limit");
+ $this->UserGrid->VirtualItemCount=$this->DataAccess->queryUserCount('');
+ $this->UserGrid->dataBind();
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ $this->bindData();
+ }
+
+ public function changePage($sender,$param)
+ {
+ $this->UserGrid->CurrentPageIndex=$param->NewPageIndex;
+ $this->bindData();
+ }
+
+ public function pagerCreated($sender,$param)
+ {
+ $param->Pager->Controls->insertAt(0,'Page: ');
+ }
+
+ public function editItem($sender,$param)
+ {
+ $this->UserGrid->EditItemIndex=$param->Item->ItemIndex;
+ $this->bindData();
+ }
+
+ public function saveItem($sender,$param)
+ {
+ $item=$param->Item;
+ $userID=$this->UserGrid->DataKeys[$item->ItemIndex];
+ $userRecord=$this->DataAccess->queryUserByID($userID);
+ $userRecord->Role=TPropertyValue::ensureInteger($item->Cells[1]->UserRole->SelectedValue);
+ $userRecord->Status=TPropertyValue::ensureInteger($item->Cells[2]->UserStatus->SelectedValue);
+ $this->DataAccess->updateUser($userRecord);
+ $this->UserGrid->EditItemIndex=-1;
+ $this->bindData();
+ }
+
+ public function cancelItem($sender,$param)
+ {
+ $this->UserGrid->EditItemIndex=-1;
+ $this->bindData();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/config.xml b/demos/blog/protected/Pages/Admin/config.xml
new file mode 100644
index 00000000..c99e5892
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/config.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+ <authorization>
+ <allow roles="admin" />
+ <deny users="*" />
+ </authorization>
+</configuration> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/ErrorReport.page b/demos/blog/protected/Pages/ErrorReport.page
new file mode 100644
index 00000000..a9b461d9
--- /dev/null
+++ b/demos/blog/protected/Pages/ErrorReport.page
@@ -0,0 +1,15 @@
+<com:TContent ID="Main">
+
+<h2>Error</h2>
+
+<p>
+<%= $this->ErrorMessage %>
+</p>
+
+<p>
+Please <a href="mailto:<%$ AdminEmail %>">report to us</a>
+if you believe this error is caused by our system. Thanks!
+</p>
+
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/ErrorReport.php b/demos/blog/protected/Pages/ErrorReport.php
new file mode 100644
index 00000000..3b24170f
--- /dev/null
+++ b/demos/blog/protected/Pages/ErrorReport.php
@@ -0,0 +1,12 @@
+<?php
+
+class ErrorReport extends BlogPage
+{
+ public function getErrorMessage()
+ {
+ $id=TPropertyValue::ensureInteger($this->Request['id']);
+ return BlogErrors::getMessage($id);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditCategory.page b/demos/blog/protected/Pages/Posts/EditCategory.page
new file mode 100644
index 00000000..fdde2648
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditCategory.page
@@ -0,0 +1,36 @@
+<com:TContent ID="Main">
+
+<h2>Update Post Category</h2>
+
+<span class="input-label">Category name</span>
+<com:TRequiredFieldValidator
+ Display="Dynamic"
+ ControlToValidate="CategoryName"
+ ErrorMessage="...is required"
+ ValidationGroup="category" />
+<com:TCustomValidator
+ ControlToValidate="CategoryName"
+ ValidationGroup="category"
+ Display="Dynamic"
+ OnServerValidate="checkCategoryName"
+ Text="...must be unique"
+ ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CategoryName" Columns="50" MaxLength="128" />
+<br/>
+
+<span class="input-label">Description</span>
+<br/>
+<com:TTextBox
+ ID="CategoryDescription"
+ TextMode="MultiLine"
+ Columns="50"
+ Rows="5" />
+<br/>
+
+<com:TLinkButton
+ Text="Save"
+ OnClick="saveButtonClicked"
+ ValidationGroup="category" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditCategory.php b/demos/blog/protected/Pages/Posts/EditCategory.php
new file mode 100644
index 00000000..fd2d0707
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditCategory.php
@@ -0,0 +1,44 @@
+<?php
+
+class EditCategory extends BlogPage
+{
+ public function getCurrentCategory()
+ {
+ $id=TPropertyValue::ensureInteger($this->Request['id']);
+ if(($cat=$this->DataAccess->queryCategoryByID($id))!==null)
+ return $cat;
+ else
+ throw new BlogException('xxx');
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ {
+ $catRecord=$this->getCurrentCategory();
+ $this->CategoryName->Text=$catRecord->Name;
+ $this->CategoryDescription->Text=$catRecord->Description;
+ }
+ }
+
+ public function saveButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $categoryRecord=$this->getCurrentCategory();
+ $categoryRecord->Name=$this->CategoryName->Text;
+ $categoryRecord->Description=$this->CategoryDescription->Text;
+ $this->DataAccess->updateCategory($categoryRecord);
+ $this->gotoPage('Posts.ListPost',array('cat'=>$categoryRecord->ID));
+ }
+ }
+
+ public function checkCategoryName($sender,$param)
+ {
+ $name=$this->CategoryName->Text;
+ $param->IsValid=$this->DataAccess->queryCategoryByName($name)===null;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditPost.page b/demos/blog/protected/Pages/Posts/EditPost.page
new file mode 100644
index 00000000..591f5945
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditPost.page
@@ -0,0 +1,41 @@
+<com:TContent ID="Main">
+
+<h2>Update Post</h2>
+
+Title
+<com:TRequiredFieldValidator
+Display="Dynamic"
+ ControlToValidate="Title"
+ ErrorMessage="...is required"
+ ValidationGroup="post" />
+<br/>
+<com:TTextBox ID="Title" Columns="70" MaxLength="256" />
+<br/>
+
+Content
+<com:TRequiredFieldValidator
+Display="Dynamic"
+ ControlToValidate="Content"
+ ErrorMessage="...is required"
+ ValidationGroup="post" />
+<br/>
+<com:THtmlArea ID="Content" Width="450px" />
+<br/>
+
+Categories<br/>
+<com:TListBox
+ ID="Categories"
+ SelectionMode="Multiple"
+ DataTextField="Name"
+ DataValueField="ID" />
+<br/>
+
+<com:TCheckBox ID="DraftMode" Text="in draft mode (the post will not be published)" />
+<br/>
+
+<com:TLinkButton
+ Text="Save"
+ OnClick="saveButtonClicked"
+ ValidationGroup="post" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditPost.php b/demos/blog/protected/Pages/Posts/EditPost.php
new file mode 100644
index 00000000..57e92b1c
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditPost.php
@@ -0,0 +1,51 @@
+<?php
+
+class EditPost extends BlogPage
+{
+ public function getCurrentPost()
+ {
+ $id=TPropertyValue::ensureInteger($this->Request['id']);
+ if(($post=$this->DataAccess->queryPostByID($id))!==null)
+ return $post;
+ else
+ throw new BlogException('xxx');
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ {
+ $postRecord=$this->getCurrentPost();
+ $this->Title->Text=$postRecord->Title;
+ $this->Content->Text=$postRecord->Content;
+ $this->DraftMode->Checked=$postRecord->Status===0;
+ $this->Categories->DataSource=$this->DataAccess->queryCategories();
+ $this->Categories->dataBind();
+ $cats=$this->DataAccess->queryCategoriesByPostID($postRecord->ID);
+ $catIDs=array();
+ foreach($cats as $cat)
+ $catIDs[]=$cat->ID;
+ $this->Categories->SelectedValues=$catIDs;
+ }
+ }
+
+ public function saveButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $postRecord=$this->getCurrentPost();
+ $postRecord->Title=$this->Title->Text;
+ $postRecord->Content=$this->Content->Text;
+ $postRecord->Status=$this->DraftMode->Checked?0:1;
+ $postRecord->ModifyTime=time();
+ $cats=array();
+ foreach($this->Categories->SelectedValues as $value)
+ $cats[]=TPropertyValue::ensureInteger($value);
+ $this->DataAccess->updatePost($postRecord,$cats);
+ $this->gotoPage('Posts.ViewPost',array('id'=>$postRecord->ID));
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ListPost.page b/demos/blog/protected/Pages/Posts/ListPost.page
new file mode 100644
index 00000000..15fc3d0c
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ListPost.page
@@ -0,0 +1,27 @@
+<com:TContent ID="Main">
+
+<com:TRepeater ID="PostList" EnableViewState="false">
+ <prop:ItemTemplate>
+<div class="post">
+<div class="post-title">
+<%# $this->DataItem->Title %>
+</div>
+<div class="post-time">
+<%# date('l, F j, Y \a\t h:i:s a',$this->DataItem->CreateTime) %>
+</div>
+<div class="post-content">
+<%# $this->DataItem->Content %>
+</div>
+<div class="post-footer">
+posted by
+<%# '<a href="' . $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->DataItem->AuthorID)) . '">' . $this->DataItem->AuthorName . '</a>' %>
+|
+<%# '<a href="' . $this->Service->constructUrl('Posts.ViewPost',array('id'=>$this->DataItem->ID)) . '">PermaLink</a>' %>
+|
+<%# '<a href="' . $this->Service->constructUrl('Posts.ViewPost',array('id'=>$this->DataItem->ID)) . '#comments">Comments (' . $this->DataItem->CommentCount . ')</a>' %>
+</div>
+</div>
+ </prop:ItemTemplate>
+</com:TRepeater>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ListPost.php b/demos/blog/protected/Pages/Posts/ListPost.php
new file mode 100644
index 00000000..6d56b543
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ListPost.php
@@ -0,0 +1,44 @@
+<?php
+
+class ListPost extends BlogPage
+{
+ const DEFAULT_LIMIT=10;
+
+ public function getPosts()
+ {
+ $timeFilter='';
+ $catFilter='';
+ if(($time=TPropertyValue::ensureInteger($this->Request['time']))>0)
+ {
+ $year=(integer)($time/100);
+ $month=$time%100;
+ $startTime=mktime(0,0,0,$month,1,$year);
+ if(++$month>12)
+ {
+ $month=1;
+ $year++;
+ }
+ $endTime=mktime(0,0,0,$month,1,$year);
+ $timeFilter="create_time>=$startTime AND create_time<$endTime";
+ }
+ if(($catID=$this->Request['cat'])!==null)
+ {
+ $catID=TPropertyValue::ensureInteger($catID);
+ $catFilter="category_id=$catID";
+ }
+ if(($offset=TPropertyValue::ensureInteger($this->Request['offset']))<=0)
+ $offset=0;
+ if(($limit=TPropertyValue::ensureInteger($this->Request['limit']))<=0)
+ $limit=self::DEFAULT_LIMIT;
+ return $this->DataAccess->queryPosts('',$timeFilter,$catFilter,'ORDER BY create_time DESC',"LIMIT $offset,$limit");
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ $this->PostList->DataSource=$this->getPosts();
+ $this->PostList->dataBind();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/MyPost.page b/demos/blog/protected/Pages/Posts/MyPost.page
new file mode 100644
index 00000000..95a32ac9
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/MyPost.page
@@ -0,0 +1,46 @@
+<com:TContent ID="Main">
+
+<h2>My Posts</h2>
+
+<com:TDataGrid ID="PostGrid"
+ AutoGenerateColumns="false"
+ CssClass="grid"
+ HeaderStyle.CssClass="grid-header"
+ ItemStyle.CssClass="grid-row1"
+ AlternatingItemStyle.CssClass="grid-row2"
+ AllowPaging="true"
+ AllowCustomPaging="true"
+ PageSize="20"
+ PagerStyle.CssClass="grid-pager"
+ PagerStyle.Mode="Numeric"
+ OnPageIndexChanged="changePage"
+ OnPagerCreated="pagerCreated"
+ >
+ <com:THyperLinkColumn
+ HeaderText="Title"
+ DataNavigateUrlField="ID"
+ DataNavigateUrlFormatString="#$this->Service->constructUrl('Posts.ViewPost',array('id'=>{0}))"
+ DataTextField="Title"
+ />
+ <com:TBoundColumn
+ HeaderText="Status"
+ DataField="Status"
+ DataFormatString="#{0}?'Published':'Draft'"
+ ItemStyle.Width="70px"
+ />
+ <com:TBoundColumn
+ HeaderText="Comments"
+ DataField="CommentCount"
+ ItemStyle.HorizontalAlign="Center"
+ ItemStyle.Width="80px"
+ />
+ <com:TBoundColumn
+ HeaderText="Time"
+ DataField="CreateTime"
+ DataFormatString="#date('M j, Y',{0})"
+ ItemStyle.Wrap="false"
+ ItemStyle.Width="90px"
+ />
+</com:TDataGrid>
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/MyPost.php b/demos/blog/protected/Pages/Posts/MyPost.php
new file mode 100644
index 00000000..be03ca63
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/MyPost.php
@@ -0,0 +1,34 @@
+<?php
+
+class MyPost extends BlogPage
+{
+ protected function bindData()
+ {
+ $author=$this->User->ID;
+ $offset=$this->PostGrid->CurrentPageIndex*$this->PostGrid->PageSize;
+ $limit=$this->PostGrid->PageSize;
+ $this->PostGrid->DataSource=$this->DataAccess->queryPosts("author_id=$author",'','','ORDER BY a.status ASC, create_time DESC',"LIMIT $offset,$limit");
+ $this->PostGrid->VirtualItemCount=$this->DataAccess->queryPostCount("author_id=$author",'','');
+ $this->PostGrid->dataBind();
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ $this->bindData();
+ }
+
+ public function changePage($sender,$param)
+ {
+ $this->PostGrid->CurrentPageIndex=$param->NewPageIndex;
+ $this->bindData();
+ }
+
+ public function pagerCreated($sender,$param)
+ {
+ $param->Pager->Controls->insertAt(0,'Page: ');
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewCategory.page b/demos/blog/protected/Pages/Posts/NewCategory.page
new file mode 100644
index 00000000..92fe1468
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewCategory.page
@@ -0,0 +1,36 @@
+<com:TContent ID="Main">
+
+<h2>New Post Category</h2>
+
+<span class="input-label">Category name</span>
+<com:TRequiredFieldValidator
+ Display="Dynamic"
+ ControlToValidate="CategoryName"
+ ErrorMessage="...is required"
+ ValidationGroup="category" />
+<com:TCustomValidator
+ ControlToValidate="CategoryName"
+ ValidationGroup="category"
+ Display="Dynamic"
+ OnServerValidate="checkCategoryName"
+ Text="...must be unique"
+ ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CategoryName" Columns="50" MaxLength="128" />
+<br/>
+
+<span class="input-label">Description</span>
+<br/>
+<com:TTextBox
+ ID="CategoryDescription"
+ TextMode="MultiLine"
+ Columns="50"
+ Rows="5" />
+<br/>
+
+<com:TLinkButton
+ Text="Save"
+ OnClick="saveButtonClicked"
+ ValidationGroup="category" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewCategory.php b/demos/blog/protected/Pages/Posts/NewCategory.php
new file mode 100644
index 00000000..d36f6af1
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewCategory.php
@@ -0,0 +1,24 @@
+<?php
+
+class NewCategory extends BlogPage
+{
+ public function saveButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $categoryRecord=new CategoryRecord;
+ $categoryRecord->Name=$this->CategoryName->Text;
+ $categoryRecord->Description=$this->CategoryDescription->Text;
+ $this->DataAccess->insertCategory($categoryRecord);
+ $this->gotoPage('Posts.ListPost',array('cat'=>$categoryRecord->ID));
+ }
+ }
+
+ public function checkCategoryName($sender,$param)
+ {
+ $name=$this->CategoryName->Text;
+ $param->IsValid=$this->DataAccess->queryCategoryByName($name)===null;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewPost.page b/demos/blog/protected/Pages/Posts/NewPost.page
new file mode 100644
index 00000000..a49188f6
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewPost.page
@@ -0,0 +1,41 @@
+<com:TContent ID="Main">
+
+<h2>Write a New Post</h2>
+
+Title
+<com:TRequiredFieldValidator
+Display="Dynamic"
+ ControlToValidate="Title"
+ ErrorMessage="...is required"
+ ValidationGroup="post" />
+<br/>
+<com:TTextBox ID="Title" Columns="70" MaxLength="256" />
+<br/>
+
+Content
+<com:TRequiredFieldValidator
+Display="Dynamic"
+ ControlToValidate="Content"
+ ErrorMessage="...is required"
+ ValidationGroup="post" />
+<br/>
+<com:THtmlArea ID="Content" Width="450px" />
+<br/>
+
+Categories<br/>
+<com:TListBox
+ ID="Categories"
+ SelectionMode="Multiple"
+ DataTextField="Name"
+ DataValueField="ID" />
+<br/>
+
+<com:TCheckBox ID="DraftMode" Text="in draft mode (the post will not be published)" />
+<br/>
+
+<com:TLinkButton
+ Text="Save"
+ OnClick="saveButtonClicked"
+ ValidationGroup="post" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewPost.php b/demos/blog/protected/Pages/Posts/NewPost.php
new file mode 100644
index 00000000..055c7f92
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewPost.php
@@ -0,0 +1,34 @@
+<?php
+
+class NewPost extends BlogPage
+{
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ {
+ $this->Categories->DataSource=$this->DataAccess->queryCategories();
+ $this->Categories->dataBind();
+ }
+ }
+
+ public function saveButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $postRecord=new PostRecord;
+ $postRecord->Title=$this->Title->Text;
+ $postRecord->Content=$this->Content->Text;
+ $postRecord->Status=$this->DraftMode->Checked?0:1;
+ $postRecord->CreateTime=time();
+ $postRecord->AuthorID=$this->User->ID;
+ $cats=array();
+ foreach($this->Categories->SelectedValues as $value)
+ $cats[]=TPropertyValue::ensureInteger($value);
+ $this->DataAccess->insertPost($postRecord,$cats);
+ $this->gotoPage('Posts.ViewPost',array('id'=>$postRecord->ID));
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ViewPost.page b/demos/blog/protected/Pages/Posts/ViewPost.page
new file mode 100644
index 00000000..4b233615
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ViewPost.page
@@ -0,0 +1,113 @@
+<com:TContent ID="Main">
+
+<div class="post">
+<div class="post-title">
+<%= $this->CurrentPost->Title %>
+</div>
+<div class="post-time">
+<%= date('l, F j, Y \a\t h:i:s a',$this->CurrentPost->CreateTime) %>
+by
+<%= '<a href="' . $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->CurrentPost->AuthorID)) . '">' . $this->CurrentPost->AuthorName . '</a>' %>
+<%= $this->CanEditPost ? '| <a href="' . $this->Service->constructUrl('Posts.EditPost',array('id'=>$this->CurrentPost->ID)) . '">Edit</a> | ' : '';
+%>
+<com:TLinkButton
+ Text="Delete"
+ OnClick="deleteButtonClicked"
+ Visible=<%= $this->CanEditPost %>
+ Attributes.onclick="if(!confirm('Are you sure to delete this post? This will also delete all related comments.')) return false;"
+ />
+</div>
+<div class="post-content">
+<%= $this->CurrentPost->Content %>
+</div>
+<div class="post-footer">
+<com:TRepeater ID="CategoryList" EnableViewState="false">
+ <prop:ItemTemplate>
+ [
+ <a href="<%# $this->Service->constructUrl('Posts.ListPost',array('cat'=>$this->DataItem->ID)) %>"><%# $this->DataItem->Name %></a>
+ ]
+ </prop:ItemTemplate>
+</com:TRepeater>
+</div>
+</div>
+
+<div class="comments">
+<a name="comments"></a>
+<h3>Comments</h3>
+
+<com:TRepeater ID="CommentList" OnItemCommand="repeaterItemCommand">
+ <prop:ItemTemplate>
+<div class="comment">
+<div class="comment-header">
+<com:TLinkButton
+ Text="Delete"
+ Attributes.onclick="if(!confirm('Are you sure to delete this comment?')) return false;"
+ CommandParameter=<%# $this->DataItem->ID %>
+ Visible=<%= $this->Page->CanEditPost %> Style="float:right"/>
+<%# date('F j, Y \a\t h:i:s a',$this->DataItem->CreateTime) %>
+by
+<%# $this->DataItem->AuthorWebsite==='' ?
+ $this->DataItem->AuthorName :
+ '<a href="' . $this->DataItem->AuthorWebsite . '">' . $this->DataItem->AuthorName . '</a>' %>
+</div>
+<div class="comment-content">
+<%# $this->DataItem->Content %>
+</div>
+</div>
+ </prop:ItemTemplate>
+</com:TRepeater>
+
+<h4>Leave your comment</h4>
+
+<span class="input-label">Name</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="CommentAuthor"
+ ValidationGroup="comment""
+ Display="Dynamic"
+ Text="...is required"
+ ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentAuthor" />
+<br/>
+
+<span class="input-label">Email address</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="CommentEmail"
+ ValidationGroup="comment""
+ Display="Dynamic"
+ Text="...is required"
+ ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+ ControlToValidate="CommentEmail"
+ ValidationGroup="comment"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="You entered an invalid email address."
+ ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentEmail" />
+<br/>
+
+<span class="input-label">Personal website</span>
+<br/>
+<com:TTextBox ID="CommentWebsite" Columns="70"/>
+<br/>
+
+<span class="input-label">Comment</span>
+<com:TRequiredFieldValidator
+ ControlToValidate="CommentContent"
+ ValidationGroup="comment"
+ Display="Dynamic"
+ Text="...is required"
+ ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentContent" TextMode="MultiLine" Columns="55" Rows="10"/>
+<br/>
+
+<com:TLinkButton
+ Text="Submit"
+ ValidationGroup="comment"
+ OnClick="submitCommentButtonClicked" />
+
+</div>
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ViewPost.php b/demos/blog/protected/Pages/Posts/ViewPost.php
new file mode 100644
index 00000000..309bedc1
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ViewPost.php
@@ -0,0 +1,73 @@
+<?php
+
+class ViewPost extends BlogPage
+{
+ private $_postID=null;
+ private $_post=null;
+
+ public function getPostID()
+ {
+ if($this->_postID===null)
+ $this->_postID=TPropertyValue::ensureInteger($this->Request['id']);
+ return $this->_postID;
+ }
+
+ public function getCurrentPost()
+ {
+ if($this->_post===null)
+ {
+ if(($this->_post=$this->DataAccess->queryPostByID($this->getPostID()))===null)
+ $this->reportError(BlogErrors::ERROR_POST_NOT_FOUND);
+ }
+ return $this->_post;
+ }
+
+ public function getCanEditPost()
+ {
+ $user=$this->getUser();
+ $authorID=$this->getCurrentPost()->AuthorID;
+ return $authorID===$user->getID() || $user->isInRole('admin');
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ $this->CategoryList->DataSource=$this->DataAccess->queryCategoriesByPostID($this->getPostID());
+ $this->CategoryList->dataBind();
+ $this->CommentList->DataSource=$this->DataAccess->queryCommentsByPostID($this->getPostID());
+ $this->CommentList->dataBind();
+ }
+
+ public function submitCommentButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $commentRecord=new CommentRecord;
+ $commentRecord->PostID=$this->CurrentPost->ID;
+ $commentRecord->AuthorName=$this->CommentAuthor->Text;
+ $commentRecord->AuthorEmail=$this->CommentEmail->Text;
+ $commentRecord->AuthorWebsite=$this->CommentWebsite->Text;
+ $commentRecord->AuthorIP=$this->Request->UserHostAddress;
+ $commentRecord->Content=$this->CommentContent->Text;
+ $commentRecord->CreateTime=time();
+ $commentRecord->Status=0;
+ $this->DataAccess->insertComment($commentRecord);
+ $this->Response->reload();
+ }
+ }
+
+ public function deleteButtonClicked($sender,$param)
+ {
+ $this->DataAccess->deletePost($this->PostID);
+ $this->gotoDefaultPage();
+ }
+
+ public function repeaterItemCommand($sender,$param)
+ {
+ $id=TPropertyValue::ensureInteger($param->CommandParameter);
+ $this->DataAccess->deleteComment($id);
+ $this->Response->reload();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/config.xml b/demos/blog/protected/Pages/Posts/config.xml
new file mode 100644
index 00000000..1c04e946
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+ <authorization>
+ <deny pages="EditPost,NewPost,MyPost" users="?" />
+ </authorization>
+</configuration> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/EditUser.page b/demos/blog/protected/Pages/Users/EditUser.page
new file mode 100644
index 00000000..8c21fd50
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/EditUser.page
@@ -0,0 +1,74 @@
+<com:TContent ID="Main">
+
+<h2>Update Profile</h2>
+
+<com:TValidationSummary Display="Dynamic" ValidationGroup="user" />
+
+<span class="input-label">Username</span>
+<br/>
+<com:TLabel ID="Username" />
+
+<br/>
+
+<span class="input-label">Full name</span>
+<br/>
+<com:TTextBox ID="FullName" />
+
+<br/>
+
+<span class="input-label">Password</span>
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+<com:TRegularExpressionValidator
+ ControlToValidate="Password"
+ ValidationGroup="user"
+ Display="Dynamic"
+ RegularExpression="[\w\.]{6,16}"
+ Text="*"
+ ErrorMessage="Your password must contain only letters, digits and underscores, and it must contain at least 6 and at most 16 characters."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Re-type Password</span>
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+<com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ValidationGroup="user"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="Your password entries did not match."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Email Address</span>
+<br/>
+<com:TTextBox ID="Email" />
+<com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ValidationGroup="user"
+ Text="*"
+ ErrorMessage="Please provide your email address."
+ ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ValidationGroup="user"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="You entered an invalid email address."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Personal Website</span>
+<br/>
+<com:TTextBox ID="Website" AutoTrim="true" />
+
+<br/>
+
+<com:TLinkButton Text="Save" ValidationGroup="user" OnClick="saveButtonClicked" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/EditUser.php b/demos/blog/protected/Pages/Users/EditUser.php
new file mode 100644
index 00000000..e3efcfd1
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/EditUser.php
@@ -0,0 +1,43 @@
+<?php
+
+class EditUser extends BlogPage
+{
+ public function getCurrentUser()
+ {
+ if(($user=$this->DataAccess->queryUserByID($this->User->ID))!==null)
+ return $user;
+ else
+ throw new BlogException('xxx');
+ }
+
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ if(!$this->IsPostBack)
+ {
+ $userRecord=$this->getCurrentUser();
+ $this->Username->Text=$userRecord->Name;
+ $this->FullName->Text=$userRecord->FullName;
+ $this->Email->Text=$userRecord->Email;
+ $this->Website->Text=$userRecord->Website;
+ }
+ }
+
+ public function saveButtonClicked($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $userRecord=$this->getCurrentUser();
+ if($this->Password->Text!=='')
+ $userRecord->Password=md5($this->Password->Text);
+ $userRecord->FullName=$this->FullName->Text;
+ $userRecord->Email=$this->Email->Text;
+ $userRecord->Website=$this->Website->Text;
+ $this->DataAccess->updateUser($userRecord);
+ $authManager=$this->Application->getModule('auth');
+ $this->gotoPage('Users.ViewUser',array('id'=>$userRecord->ID));
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/NewUser.page b/demos/blog/protected/Pages/Users/NewUser.page
new file mode 100644
index 00000000..eba2dcec
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/NewUser.page
@@ -0,0 +1,104 @@
+<com:TContent ID="Main">
+
+<h2>Create New Account</h2>
+
+<com:TValidationSummary Display="Dynamic" ValidationGroup="NewUser" />
+
+<span class="input-label">Username</span>
+<br/>
+<com:TTextBox ID="Username" />
+<com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="Please choose a username."
+ ControlCssClass="inputerror" />
+<com:TRegularExpressionValidator
+ ControlToValidate="Username"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ RegularExpression="[\w]{3,16}"
+ Text="*"
+ ErrorMessage="Your username must contain only letters, digits and underscores, and it must contain at least 3 and at most 16 characters."
+ ControlCssClass="inputerror" />
+<com:TCustomValidator
+ ControlToValidate="Username"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ OnServerValidate="checkUsername"
+ Text="*"
+ ErrorMessage="Sorry, your username is taken by someone else. Please choose another username."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Full name</span>
+<br/>
+<com:TTextBox ID="FullName" />
+
+<br/>
+
+<span class="input-label">Password</span>
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+<com:TRequiredFieldValidator
+ ControlToValidate="Password"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="Please choose a password."
+ ControlCssClass="inputerror" />
+<com:TRegularExpressionValidator
+ ControlToValidate="Password"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ RegularExpression="[\w\.]{6,16}"
+ Text="*"
+ ErrorMessage="Your password must contain only letters, digits and underscores, and it must contain at least 6 and at most 16 characters."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Re-type Password</span>
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+<com:TCompareValidator
+ ControlToValidate="Password"
+ ControlToCompare="Password2"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="Your password entries did not match."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Email Address</span>
+<br/>
+<com:TTextBox ID="Email" />
+<com:TRequiredFieldValidator
+ ControlToValidate="Email"
+ ValidationGroup="NewUser"
+ Text="*"
+ ErrorMessage="Please provide your email address."
+ ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+ ControlToValidate="Email"
+ ValidationGroup="NewUser"
+ Display="Dynamic"
+ Text="*"
+ ErrorMessage="You entered an invalid email address."
+ ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Personal Website</span>
+<br/>
+<com:TTextBox ID="Website" AutoTrim="true" />
+
+<br/>
+
+<com:TLinkButton Text="Register" ValidationGroup="NewUser" OnClick="createUser" />
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/NewUser.php b/demos/blog/protected/Pages/Users/NewUser.php
new file mode 100644
index 00000000..166abf66
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/NewUser.php
@@ -0,0 +1,31 @@
+<?php
+
+class NewUser extends BlogPage
+{
+ public function checkUsername($sender,$param)
+ {
+ $username=$this->Username->Text;
+ $param->IsValid=$this->DataAccess->queryUserByName($username)===null;
+ }
+
+ public function createUser($sender,$param)
+ {
+ if($this->IsValid)
+ {
+ $userRecord=new UserRecord;
+ $userRecord->Name=$this->Username->Text;
+ $userRecord->FullName=$this->FullName->Text;
+ $userRecord->Role=0;
+ $userRecord->Password=md5($this->Password->Text);
+ $userRecord->Email=$this->Email->Text;
+ $userRecord->CreateTime=time();
+ $userRecord->Website=$this->Website->Text;
+ $this->DataAccess->insertUser($userRecord);
+ $authManager=$this->Application->getModule('auth');
+ $authManager->login($this->Username->Text,$this->Password->Text);
+ $this->gotoDefaultPage();
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/ViewUser.page b/demos/blog/protected/Pages/Users/ViewUser.page
new file mode 100644
index 00000000..2dba6b77
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/ViewUser.page
@@ -0,0 +1,21 @@
+<com:TContent ID="Main">
+
+<h2>User Profile</h2>
+
+Username: <%= $this->CurrentUser->Name %>
+<br/>
+
+Full name: <%= $this->CurrentUser->FullName %>
+<br/>
+
+Email: <%= $this->CurrentUser->Email %>
+<br/>
+
+Privilege: <%= $this->CurrentUser->Role===0? 'User':'Administrator' %>
+<br/>
+
+Personal website: <%= $this->CurrentUser->Website %>
+<br/>
+
+
+</com:TContent> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/ViewUser.php b/demos/blog/protected/Pages/Users/ViewUser.php
new file mode 100644
index 00000000..3485f56b
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/ViewUser.php
@@ -0,0 +1,19 @@
+<?php
+
+class ViewUser extends BlogPage
+{
+ private $_currentUser=null;
+
+ public function getCurrentUser()
+ {
+ if($this->_currentUser===null)
+ {
+ $id=TPropertyValue::ensureInteger($this->Request['id']);
+ if(($this->_currentUser=$this->DataAccess->queryUserByID($id))===null)
+ throw new BlogException('xxx');
+ }
+ return $this->_currentUser;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/config.xml b/demos/blog/protected/Pages/Users/config.xml
new file mode 100644
index 00000000..df8e4ad1
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+ <authorization>
+ <deny pages="EditUser" users="?" />
+ </authorization>
+</configuration> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/AccountPortlet.php b/demos/blog/protected/Portlets/AccountPortlet.php
new file mode 100644
index 00000000..0f0e60c6
--- /dev/null
+++ b/demos/blog/protected/Portlets/AccountPortlet.php
@@ -0,0 +1,14 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class AccountPortlet extends Portlet
+{
+ public function logout($sender,$param)
+ {
+ $this->Application->getModule('auth')->logout();
+ $this->Response->reload();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/AccountPortlet.tpl b/demos/blog/protected/Portlets/AccountPortlet.tpl
new file mode 100644
index 00000000..2a401f41
--- /dev/null
+++ b/demos/blog/protected/Portlets/AccountPortlet.tpl
@@ -0,0 +1,20 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Account</h2>
+
+<div class="portlet-content">
+Welcome, <b><%= $this->User->Name %></b>!
+<ul>
+<li><a href="<%= $this->Service->constructUrl('Posts.NewPost') %>">New post</a></li>
+<li><a href="<%= $this->Service->constructUrl('Posts.MyPost') %>">My post</a></li>
+<li><a href="<%= $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->User->ID)) %>">Profile</a></li>
+<%%
+if($this->User->isInRole('admin'))
+ echo '<li><a href="'.$this->Service->constructUrl('Admin.PostMan').'">Admin</a></li>';
+%>
+<li><com:TLinkButton Text="Logout" OnClick="logout" /></li>
+</ul>
+
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/ArchivePortlet.php b/demos/blog/protected/Portlets/ArchivePortlet.php
new file mode 100644
index 00000000..a004c7a9
--- /dev/null
+++ b/demos/blog/protected/Portlets/ArchivePortlet.php
@@ -0,0 +1,45 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class ArchivePortlet extends Portlet
+{
+ private function makeMonthTime($timestamp)
+ {
+ $date=getdate($timestamp);
+ return mktime(0,0,0,$date['mon'],1,$date['year']);
+ }
+
+ public function onLoad($param)
+ {
+ $currentTime=time();
+ $startTime=$this->Application->getModule('data')->queryEarliestPostTime();
+ if(empty($startTime)) // if no posts
+ $startTime=$currentTime;
+
+ // obtain the timestamp for the initial month
+ $date=getdate($startTime);
+ $startTime=mktime(0,0,0,$date['mon'],1,$date['year']);
+
+ $date=getdate($currentTime);
+ $month=$date['mon'];
+ $year=$date['year'];
+
+ $timestamps=array();
+ while(true)
+ {
+ if(($timestamp=mktime(0,0,0,$month,1,$year))<$startTime)
+ break;
+ $timestamps[]=$timestamp;
+ if(--$month===0)
+ {
+ $month=12;
+ $year--;
+ }
+ }
+ $this->MonthList->DataSource=$timestamps;
+ $this->MonthList->dataBind();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/ArchivePortlet.tpl b/demos/blog/protected/Portlets/ArchivePortlet.tpl
new file mode 100644
index 00000000..c576e9f5
--- /dev/null
+++ b/demos/blog/protected/Portlets/ArchivePortlet.tpl
@@ -0,0 +1,15 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Archives</h2>
+
+<div class="portlet-content">
+<ul>
+<com:TRepeater ID="MonthList" EnableViewState="false">
+ <prop:ItemTemplate>
+ <li><a href="<%# $this->Service->constructUrl('Posts.ListPost',array('time'=>date('Ym',$this->DataItem))) %>"><%# date('F Y',$this->DataItem) %></a></li>
+ </prop:ItemTemplate>
+</com:TRepeater>
+</ul>
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/CategoryPortlet.php b/demos/blog/protected/Portlets/CategoryPortlet.php
new file mode 100644
index 00000000..9c2033aa
--- /dev/null
+++ b/demos/blog/protected/Portlets/CategoryPortlet.php
@@ -0,0 +1,15 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class CategoryPortlet extends Portlet
+{
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ $this->CategoryList->DataSource=$this->Application->getModule('data')->queryCategories();
+ $this->CategoryList->dataBind();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/CategoryPortlet.tpl b/demos/blog/protected/Portlets/CategoryPortlet.tpl
new file mode 100644
index 00000000..acbd3bec
--- /dev/null
+++ b/demos/blog/protected/Portlets/CategoryPortlet.tpl
@@ -0,0 +1,24 @@
+<div class="portlet">
+
+<h2 class="portlet-title">
+Categories
+<com:THyperLink
+ Text="[+]"
+ Tooltip="Create a new category"
+ NavigateUrl=<%= $this->Service->constructUrl('Posts.NewCategory') %>
+ Visible=<%= $this->User->isInRole('admin') %> />
+</h2>
+
+<div class="portlet-content">
+<ul>
+<com:TRepeater ID="CategoryList" EnableViewState="false">
+ <prop:ItemTemplate>
+ <li>
+ <a href="<%# $this->Service->constructUrl('Posts.ListPost',array('cat'=>$this->DataItem->ID)) %>"><%# $this->DataItem->Name . ' (' . $this->DataItem->PostCount . ')' %></a>
+ </li>
+ </prop:ItemTemplate>
+</com:TRepeater>
+</ul>
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/LoginPortlet.php b/demos/blog/protected/Portlets/LoginPortlet.php
new file mode 100644
index 00000000..0085c17f
--- /dev/null
+++ b/demos/blog/protected/Portlets/LoginPortlet.php
@@ -0,0 +1,22 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class LoginPortlet extends Portlet
+{
+ public function validateUser($sender,$param)
+ {
+ $authManager=$this->Application->getModule('auth');
+ if(!$authManager->login($this->Username->Text,$this->Password->Text))
+ $param->IsValid=false;
+ }
+
+ public function loginButtonClicked($sender,$param)
+ {
+ if($this->Page->IsValid)
+ $this->Response->reload();
+ //$this->Response->redirect($this->Application->getModule('auth')->getReturnUrl());
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/LoginPortlet.tpl b/demos/blog/protected/Portlets/LoginPortlet.tpl
new file mode 100644
index 00000000..6f8c5d4a
--- /dev/null
+++ b/demos/blog/protected/Portlets/LoginPortlet.tpl
@@ -0,0 +1,36 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Login</h2>
+
+<com:TPanel CssClass="portlet-content" DefaultButton="LoginButton">
+Username
+<com:TRequiredFieldValidator
+ ControlToValidate="Username"
+ ValidationGroup="login"
+ Text="...is required"
+ Display="Dynamic"/>
+<br/>
+<com:TTextBox ID="Username" />
+<br/>
+
+Password
+<com:TCustomValidator
+ ControlToValidate="Password"
+ ValidationGroup="login"
+ Text="...is invalid"
+ Display="Dynamic"
+ OnServerValidate="validateUser" />
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<com:TLinkButton
+ ID="LoginButton"
+ Text="Login"
+ ValidationGroup="login"
+ OnClick="loginButtonClicked" />
+| <a href="<%= $this->Service->constructUrl('Users.NewUser') %>">Register</a>
+
+</com:TPanel><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/Portlet.php b/demos/blog/protected/Portlets/Portlet.php
new file mode 100644
index 00000000..4b1c80e9
--- /dev/null
+++ b/demos/blog/protected/Portlets/Portlet.php
@@ -0,0 +1,7 @@
+<?php
+
+class Portlet extends TTemplateControl
+{
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/SearchPortlet.php b/demos/blog/protected/Portlets/SearchPortlet.php
new file mode 100644
index 00000000..1bad7f1c
--- /dev/null
+++ b/demos/blog/protected/Portlets/SearchPortlet.php
@@ -0,0 +1,22 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class SearchPortlet extends Portlet
+{
+ public function onInit($param)
+ {
+ parent::onInit($param);
+ if(!$this->Page->IsPostBack && ($keyword=$this->Request['keyword'])!==null)
+ $this->Keyword->Text=$keyword;
+ }
+
+ public function search($sender,$param)
+ {
+ $keyword=$this->Keyword->Text;
+ $url=$this->Service->constructUrl('SearchPost',array('keyword'=>$keyword));
+ $this->Response->redirect($url);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/demos/blog/protected/Portlets/SearchPortlet.tpl b/demos/blog/protected/Portlets/SearchPortlet.tpl
new file mode 100644
index 00000000..f88fca7e
--- /dev/null
+++ b/demos/blog/protected/Portlets/SearchPortlet.tpl
@@ -0,0 +1,21 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Search</h2>
+
+<com:TPanel CssClass="portlet-content" DefaultButton="SearchButton">
+Keyword
+<com:TRequiredFieldValidator
+ ControlToValidate="Keyword"
+ ValidationGroup="search"
+ Text="...is required"
+ Display="Dynamic"/>
+<br/>
+<com:TTextBox ID="Keyword" />
+<com:TLinkButton
+ ID="SearchButton"
+ Text="Search"
+ ValidationGroup="search"
+ OnClick="search" />
+</com:TPanel><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/application.xml b/demos/blog/protected/application.xml
new file mode 100644
index 00000000..9bca115c
--- /dev/null
+++ b/demos/blog/protected/application.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<application id="personal" mode="Debug">
+ <paths>
+ <using namespace="Application.Common.*" />
+ </paths>
+ <!-- modules configured and loaded for all services -->
+ <modules>
+ <!-- Remove this comment mark to enable caching
+ <module id="cache" class="System.Caching.TSqliteCache" />
+ -->
+ <!-- Remove this comment mark to enable PATH url format
+ <module id="request" class="THttpRequest" UrlFormat="Path" />
+ -->
+ <!--
+ <module id="session" class="THttpSession" />
+ <module id="log" class="System.Util.TLogRouter">
+ <route class="TBrowserLogRoute" />
+ <route class="TFileLogRoute" Categories="System" Levels="Notice,Warning,Error,Alert,Fatal" />
+ </module>
+ -->
+ <module class="System.Util.TParameterModule" ParameterFile="Application.Data.Options" />
+ </modules>
+ <services>
+ <!-- page service -->
+ <service id="page" class="TPageService" BasePath="Application.Pages" DefaultPage="Posts.ListPost">
+ <!-- modules configured and loaded when page service is requested -->
+ <modules>
+ <!-- user manager module -->
+ <module id="users" class="Application.Common.BlogUserManager" />
+ <!-- auth manager module -->
+ <module id="auth" class="System.Security.TAuthManager" UserManager="users" LoginPage="Posts.ListPost" />
+ <module id="data" class="Application.Common.BlogDataModule" />
+ </modules>
+ <pages MasterClass="Application.Layouts.MainLayout" Theme="Basic" />
+ </service>
+ </services>
+</application> \ No newline at end of file
diff --git a/demos/blog/sitemap.txt b/demos/blog/sitemap.txt
new file mode 100644
index 00000000..8326855b
--- /dev/null
+++ b/demos/blog/sitemap.txt
@@ -0,0 +1,106 @@
+Be careful about username case sensitivity!!
+
+Home : list of blogs filtered by a category or time range, with paging
+
+ViewBlog : read a single blog with all comments and a comment input form
+NewBlog : create a new blog, with file attachment form and THtmlArea
+EditBlog : edit an existing blog
+
+LoginUser : login page
+NewUser : create a new user
+EditUser : edit the current user
+
+Admin : whether allow multiple users, whether HTML is allowed (first user is always the admin)
+
+URL design:
+
+index.php?page=ListBlog&timespan=123,456&limit=123,456 : list of latest blogs, equivalent to:
+index.php?page=NewBlog
+index.php?page=EditBlog&id=123
+index.php?page=ViewBlog&id=123
+index.php?page=NewUser
+index.php?page=EditUser
+index.php?page=ViewUser
+index.php?page=Admin
+
+
+Use Case 1: Add a post
+1. Authorization check
+2. display UI for adding post
+3. input validation
+4. add post to DB
+5. display UI for post list
+
+
+DB Logic needed:
+
+class Post extends DataObject
+{
+ public $xxx;
+}
+
+class Comment extends DataObject
+{
+}
+
+class UserProfile extends DataObject
+{
+
+}
+
+class DataObject extends TComponent
+{
+ protected static $mapping=array();
+
+ public function __construct($db)
+ {
+ }
+
+ protected static function generateModifier($filter,$orderBy,$limit)
+ {
+ $modifier='';
+ if($filter!=='')
+ $modifier=' WHERE '.$filter;
+ if($orderBy!=='')
+ $modifier.=' ORDER BY '.$orderBy;
+ if($limit!=='')
+ $modifier.=' LIMIT '.$limit;
+ return $modifier;
+ }
+
+ public static function queryRow($filter='')
+ {
+ $modifier=self::generateModifier($filter,'','');
+ }
+
+ public static function query($filter='',$orderBy='',$limit='')
+ {
+ $modifier=self::generateModifier($filter,$orderBy,$limit);
+ }
+
+ public function save()
+ {
+ }
+
+ public function delete()
+ {
+ }
+}
+
+public function queryUsers($filter='',$sortBy='',$limit='')
+public function queryUser($id)
+public function insertUser($user)
+public function updateUser($user)
+public function deleteUser($id)
+
+public function queryPosts($filter='',$sortBy='',$limit='')
+public function queryPost($id)
+public function insertPost($post)
+public function updatePost($post)
+public function deletePost($id)
+
+public function queryComments($filter='',$sortBy='',$limit='')
+public function queryComment($id)
+public function insertComment($comment)
+public function updateComment($comment)
+public function deleteComment($id)
diff --git a/demos/blog/themes/Basic/style.css b/demos/blog/themes/Basic/style.css
new file mode 100644
index 00000000..b8e9ca89
--- /dev/null
+++ b/demos/blog/themes/Basic/style.css
@@ -0,0 +1,261 @@
+html {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: verdana, 'trebuchet ms', sans-serif;
+ font-size: 12px;
+ color: #333;
+ background: #36414d;
+ min-width:750px;
+}
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+a {
+ text-decoration: underline;
+}
+
+a img {
+ border: 0px none;
+}
+
+#page {
+ background:#fff;
+ margin:0 auto;
+ width:800px;
+}
+
+#header {
+ background: #a3b8cc;
+ border-bottom: 1px solid silver;
+}
+
+#header h1 {
+ padding:5px;
+ padding-left: 20px;
+ margin:0;
+ font-size: 16pt;
+}
+
+#header h2 {
+ padding:5px;
+ padding-left: 20px;
+ margin:0;
+ font-size: 12pt;
+}
+
+#main {
+ background:#fff;
+ float:left;
+ width:560px;
+ padding: 20px;
+}
+
+#main h2 {
+ border-bottom: 1px solid silver;
+}
+
+#sidebar {
+ background:#e6ecf2;
+ float:right;
+ width:200px;
+}
+
+#sidebar ul {
+ margin-bottom:0;
+}
+
+#sidebar h3, #sidebar p {
+ padding:0 10px 0 0;
+}
+
+#footer {
+ background:#fff;
+ clear:both;
+ color: gray;
+ font-size:8pt;
+ text-align:center;
+ padding-top:25px;
+ padding-bottom:10px;
+}
+
+.portlet {
+ margin: 10px;
+ border-bottom: 1px solid silver;
+ border-right: 1px solid silver;
+ background: #dae0e6;
+}
+
+.portlet-title {
+ /* ie win (5, 5.5, 6) bugfix */
+ p\osition: relative;
+ width: 100%;
+ w\idth: auto;
+
+ margin: 0;
+ border-left: 5px solid #36414d;
+ border-bottom: 1px solid silver;
+ padding: 5px;
+ color: #fff;
+ background: #a3b8cc;
+ font-size: 8pt;
+ font-weight: bold;
+ line-height: 1;
+ text-transform: uppercase;
+}
+
+.portlet-title a {
+ color: yellow;
+ text-decoration: none;
+ text-transform: none;
+}
+
+.portlet-title a:hover {
+ color: red;
+}
+
+.portlet-content {
+ margin: 0 0 10px 0;
+ border-top: 1px solid #cfd4d9;
+ padding: 10px 10px 0 10px;
+ font-size: 10px;
+}
+
+.portlet-content ul {
+ margin: 0 15px 10px 15px;
+ padding: 0;
+ list-style: square;
+}
+
+.portlet-content li {
+ color: #666;
+ margin-top: 3px;
+}
+
+.portlet-content a {
+ color: #36414d;
+ text-decoration: none;
+}
+
+.portlet-content a:hover {
+ color: red;
+}
+
+.post {
+ margin-bottom: 15px;
+}
+
+.post-title {
+ font-size: 14pt;
+ border-bottom: 1px silver solid;
+}
+
+.post-time {
+ font-size: 8pt;
+ color: gray;
+}
+
+.post-content {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.post-footer {
+ text-align: right;
+ font-size: 8pt;
+}
+
+.post-footer a {
+ color: #36414d;
+}
+
+.comments {
+ border-top: 1px silver solid;
+}
+
+.comments h3 {
+ font-size: 12pt;
+}
+
+.comment {
+ margin-bottom: 10px;
+}
+
+.comment-header {
+ background-color: #e6ecf2;
+ padding: 3px;
+ font-size: 8pt;
+}
+
+.grid {
+ width: 100%;
+}
+
+.grid td {
+ padding: 3px;
+}
+
+.grid-header {
+ color: #fff;
+ background: #a3b8cc;
+}
+
+.grid-row1 {
+ background: #dae0e6;
+}
+
+.grid-row2 {
+ background: #e6ecf2;
+}
+
+.grid-row-selected {
+ background: lightyellow;
+}
+
+.grid-pager {
+ text-align: right;
+ color: silver;
+}
+
+.grid-pager a {
+ color: green;
+ text-decoration: none;
+}
+
+.submenu {
+ margin-bottom: 10px;
+ border-bottom: 5px solid #a3b8cc;
+ padding-right: 10px;
+ text-align: right;
+}
+
+.submenu ul {
+ margin:0;
+ padding:0;
+ list-style:none;
+}
+
+.submenu li {
+ display:inline;
+ margin:0;
+ padding:0;
+}
+
+.submenu-active {
+ text-decoration: none;
+ background: #a3b8cc;
+ padding: 3px 5px 0 5px;
+}
+
+.submenu-inactive {
+ text-decoration: none;
+ background: #dae0e6;
+ padding: 3px 5px 0 5px;
+} \ No newline at end of file