From dd028bec3822d1d9c28c35d599d687e038c7705f Mon Sep 17 00:00:00 2001 From: wei <> Date: Thu, 4 Jan 2007 11:23:26 +0000 Subject: Add chat demo and tutorial. --- .gitattributes | 22 + demos/chat/index.php | 29 + demos/chat/protected/.htaccess | 1 + demos/chat/protected/App_Code/ChatBufferRecord.php | 64 ++ demos/chat/protected/App_Code/ChatUserManager.php | 63 ++ demos/chat/protected/App_Code/ChatUserRecord.php | 41 ++ demos/chat/protected/App_Code/chat.db | Bin 0 -> 4096 bytes demos/chat/protected/application.xml | 24 + demos/chat/protected/pages/Home.page | 51 ++ demos/chat/protected/pages/Home.php | 56 ++ demos/chat/protected/pages/Login.page | 35 + demos/chat/protected/pages/Login.php | 40 ++ demos/chat/protected/pages/config.xml | 15 + demos/chat/protected/pages/send.gif | Bin 0 -> 993 bytes demos/chat/protected/pages/send.png | Bin 0 -> 32586 bytes demos/chat/protected/pages/style.css | 82 +++ demos/quickstart/protected/controls/TopicList.tpl | 1 + .../protected/pages/Advanced/Scripts1.page | 465 ------------- .../protected/pages/Tutorial/AjaxChat.page | 753 +++++++++++++++++++++ .../pages/Tutorial/CurrencyConverter.page | 4 +- .../quickstart/protected/pages/Tutorial/chat1.png | Bin 0 -> 10533 bytes .../quickstart/protected/pages/Tutorial/chat2.png | Bin 0 -> 13409 bytes .../Data/ActiveRecord/Exceptions/messages.txt | 3 +- framework/Data/ActiveRecord/TActiveRecord.php | 22 + .../Data/ActiveRecord/TActiveRecordGateway.php | 14 + .../Data/ActiveRecord/Vendor/TDbMetaDataCommon.php | 19 +- framework/Data/TDataSourceConfig.php | 9 + .../UI/ActiveControls/TCallbackClientScript.php | 9 + framework/Web/UI/TPage.php | 5 +- .../protected/pages/ClientSideDispatch.page | 27 + .../protected/pages/ClientSideDispatch.php | 17 + .../protected/pages/RadioButtonListTest.page | 30 + .../protected/pages/RadioButtonListTest.php | 28 + 33 files changed, 1458 insertions(+), 471 deletions(-) create mode 100644 demos/chat/index.php create mode 100644 demos/chat/protected/.htaccess create mode 100644 demos/chat/protected/App_Code/ChatBufferRecord.php create mode 100644 demos/chat/protected/App_Code/ChatUserManager.php create mode 100644 demos/chat/protected/App_Code/ChatUserRecord.php create mode 100644 demos/chat/protected/App_Code/chat.db create mode 100644 demos/chat/protected/application.xml create mode 100644 demos/chat/protected/pages/Home.page create mode 100644 demos/chat/protected/pages/Home.php create mode 100644 demos/chat/protected/pages/Login.page create mode 100644 demos/chat/protected/pages/Login.php create mode 100644 demos/chat/protected/pages/config.xml create mode 100644 demos/chat/protected/pages/send.gif create mode 100644 demos/chat/protected/pages/send.png create mode 100644 demos/chat/protected/pages/style.css create mode 100644 demos/quickstart/protected/pages/Tutorial/AjaxChat.page create mode 100644 demos/quickstart/protected/pages/Tutorial/chat1.png create mode 100644 demos/quickstart/protected/pages/Tutorial/chat2.png create mode 100644 tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page create mode 100644 tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php create mode 100644 tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page create mode 100644 tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php diff --git a/.gitattributes b/.gitattributes index 102eddc9..3ccd211a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -709,6 +709,21 @@ demos/blog/themes/Fall/style.css -text demos/blog/themes/Spring/style.css -text demos/blog/themes/Summer/style.css -text demos/blog/themes/Winter/style.css -text +demos/chat/index.php -text +demos/chat/protected/.htaccess -text +demos/chat/protected/App_Code/ChatBufferRecord.php -text +demos/chat/protected/App_Code/ChatUserManager.php -text +demos/chat/protected/App_Code/ChatUserRecord.php -text +demos/chat/protected/App_Code/chat.db -text +demos/chat/protected/application.xml -text +demos/chat/protected/pages/Home.page -text +demos/chat/protected/pages/Home.php -text +demos/chat/protected/pages/Login.page -text +demos/chat/protected/pages/Login.php -text +demos/chat/protected/pages/config.xml -text +demos/chat/protected/pages/send.gif -text +demos/chat/protected/pages/send.png -text +demos/chat/protected/pages/style.css -text demos/composer/index.php -text demos/composer/index2.php -text demos/composer/protected/.htaccess -text @@ -1116,7 +1131,10 @@ demos/quickstart/protected/pages/GettingStarted/sequence.gif -text demos/quickstart/protected/pages/GettingStarted/sequence.vsd -text demos/quickstart/protected/pages/Search.page -text demos/quickstart/protected/pages/Search.php -text +demos/quickstart/protected/pages/Tutorial/AjaxChat.page -text demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page -text +demos/quickstart/protected/pages/Tutorial/chat1.png -text +demos/quickstart/protected/pages/Tutorial/chat2.png -text demos/quickstart/protected/pages/Tutorial/example1.png -text demos/quickstart/protected/pages/Tutorial/example2.png -text demos/quickstart/protected/pages/ViewSource.page -text @@ -2043,6 +2061,8 @@ tests/FunctionalTests/active-controls/protected/pages/ActiveRedirectionTest.page tests/FunctionalTests/active-controls/protected/pages/ActiveRedirectionTest.php -text tests/FunctionalTests/active-controls/protected/pages/CallbackCustomValidatorTest.page -text tests/FunctionalTests/active-controls/protected/pages/CallbackCustomValidatorTest.php -text +tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page -text +tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateComponent.php -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateComponent.tpl -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateControlTest.page -text @@ -2077,6 +2097,8 @@ tests/FunctionalTests/active-controls/protected/pages/PopulateActiveList.page -t tests/FunctionalTests/active-controls/protected/pages/PopulateActiveList.php -text tests/FunctionalTests/active-controls/protected/pages/PostLoadingTest.page -text tests/FunctionalTests/active-controls/protected/pages/PostLoadingTest.php -text +tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page -text +tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php -text tests/FunctionalTests/active-controls/protected/pages/RatingList.page -text tests/FunctionalTests/active-controls/protected/pages/RatingList.php -text tests/FunctionalTests/active-controls/protected/pages/RepeaterWithActiveControls.page -text diff --git a/demos/chat/index.php b/demos/chat/index.php new file mode 100644 index 00000000..17ed21d0 --- /dev/null +++ b/demos/chat/index.php @@ -0,0 +1,29 @@ +run(); + +?> \ No newline at end of file diff --git a/demos/chat/protected/.htaccess b/demos/chat/protected/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/demos/chat/protected/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatBufferRecord.php b/demos/chat/protected/App_Code/ChatBufferRecord.php new file mode 100644 index 00000000..cf3c651f --- /dev/null +++ b/demos/chat/protected/App_Code/ChatBufferRecord.php @@ -0,0 +1,64 @@ +_created_on === null) + $this->_created_on = time(); + return $this->_created_on; + } + + public function setCreated_On($value) + { + $this->_created_on = $value; + } + + public static function finder() + { + return parent::getRecordFinder('ChatBufferRecord'); + } + + public function saveMessage() + { + foreach(ChatUserRecord::finder()->findAll() as $user) + { + $message = new self; + $message->for_user = $user->username; + $message->from_user = $this->from_user; + $message->message = $this->message; + $message->save(); + if($user->username == $this->from_user) + { + $user->last_activity = time(); //update the last activity; + $user->save(); + } + } + } + + public function getUserMessages($user) + { + $content = ''; + foreach($this->findAll('for_user = ?', $user) as $message) + $content .= $this->formatMessage($message); + $this->deleteAll('for_user = ? OR created_on < ?', $user, time() - 300); //5 min inactivity + return $content; + } + + protected function formatMessage($message) + { + $user = htmlspecialchars($message->from_user); + $content = htmlspecialchars($message->message); + return "
"; + } +} + +?> \ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatUserManager.php b/demos/chat/protected/App_Code/ChatUserManager.php new file mode 100644 index 00000000..f8fe09cc --- /dev/null +++ b/demos/chat/protected/App_Code/ChatUserManager.php @@ -0,0 +1,63 @@ +setIsGuest(true); + if($username !== null) + { + $user->setIsGuest(false); + $user->setName($username); + $user->setRoles(array('normal')); + } + return $user; + } + + /** + * Add a new user to the database. + * @param string username. + */ + public function addNewUser($username) + { + $user = new ChatUserRecord(); + $user->username = $username; + $user->save(); + } + + /** + * @return boolean true if username already exists, false otherwise. + */ + public function usernameExists($username) + { + return ChatUserRecord::finder()->findByUsername($username) instanceof ChatUserRecord; + } + + /** + * Validates if the username exists. + * @param string user name + * @param string password + * @return boolean true if validation is successful, false otherwise. + */ + public function validateUser($username,$password) + { + return $this->usernameExists($username); + } +} + + +?> \ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatUserRecord.php b/demos/chat/protected/App_Code/ChatUserRecord.php new file mode 100644 index 00000000..9bfb11cd --- /dev/null +++ b/demos/chat/protected/App_Code/ChatUserRecord.php @@ -0,0 +1,41 @@ +_last_activity === null) + $this->_last_activity = time(); + return $this->_last_activity; + } + + public function setLast_Activity($value) + { + $this->_last_activity = $value; + } + + public static function finder() + { + return parent::getRecordFinder('ChatUserRecord'); + } + + public function getUserList() + { + $this->deleteAll('last_activity < ?', time()-300); //5 min inactivity + $content = '- The $A() function converts the single argument it receives - into an Array object. -
-- This function, combined with the extensions for the Array class, - makes it easier to convert or copy any enumerable list into an - Array object. One suggested use is to convert DOM - NodeLists into regular arrays, which can be traversed - more efficiently. See example below. -
- -- The $H() function converts - objects into enumerable Hash objects that - resemble associative arrays. -
-- We are all familiar with for-loops. You know, create yourself an array, populate it with - elements of the same kind, create a loop control structure (for, foreach, while, repeat, etc,) - access each element sequentially, by its numeric index, and do something with the element. -
-- When you come to think about it, almost every time you have an array in your code it - means that you'll be using that array in a loop sooner or later. Wouldn't it be nice - if the array objects had more functionality to deal with these iterations? Yes, it would, - and many programming languages provide such functionality in their arrays or equivalent - structures (like collections and lists.) -
- -- Well, it turns out that prototype.js gives us the Enumerable - object, which implements a plethora of tricks for us to use when dealing with iterable data. - The prototype.js library goes one step further and extends the - Array class with all the methods of Enumerable. -
- - -- In standard javascript, if you wanted to sequentially display the elements of an array, - you could very well write something like this. -
-- With our new best friend, prototype.js, we can rewrite this loop like this. -
- -- You are probably thinking "big freaking deal...just a weird syntax for the same old thing." - Well, in the above example, yes, there's nothing too earth shattering going on. After all, - there's not much to be changed in such a drop-dead-simple example. But - keep reading, nonetheless. -
-- Before we move on. Do you see this function that is being passed as an argument - to the each method? Let's start referring to it as an - iterator function. -
- -- Like we mentioned above, it's very common for all the elements in your array to be of - the same kind, with the same properties and methods. Let's see how we can take advantage - of iterator functions with our new souped-up arrays. -
-- Finding an element according to a criteria. -
-
- Now let's kick it up another notch. See how we can filter out - items in arrays, then retrieve just a desired member from each - element. -
-
- This <a href="http://othersite.com/page.html">text</a> has
- a <a href="#localAnchor">lot</a> of
- <a href="#otherAnchor">links</a>. Some are
- <a href="http://wherever.com/page.html">external</a>
- and some are <a href="#someAnchor">local</a>
-
- It takes just a little bit of practice to get completely addicted to this syntax. - Next we will go through the available functions with the following example. -
-I used to find myself writing a lot of for loops. Although,
-Prototype does not by any means eliminate the need to do for-loops,
-it does give you access to what I consider to be a cleaner, easier to read method in each.
-
-The each function allows us to iterate over these objects Ruby style. -
-The each function takes one argument, an iterator function. -This iterator is invoked once for every item in the array, and that item -along with the optional index is passed to the iterator. So if -we also needed the index we could do something like the code below. -
- -Hashes can be created by wrapping an Object (associative array) in -$H() and can have their key/value pairs exposed.
- --We can also directly access the keys and values of a Hash without iterating over it. -
-The collect function allows you to iterate over an Array and return the -results as a new array. Each item returned as a result of the iteration will be -pushed onto the end of the new array.
-You can even join on the end of the block.
-The include function allows you to check if a value is included in an array -and returns true or false depending on if a match was made. Assuming I put -up a form asking the user to name some artist in my iTunes playlist, -we could do something like the code below. Prime candidate for some conditional madness. -
-The inject function is good for getting a collective sum from an array of -values. For instance, to add up all the numbers. -
-The first argument to inject is just an initial value that -would be added to the sum, so if we added 1 instead of 0, the output would be 162.
- --When given an Array, the findAll function will return an array of -items for which the iterator evaluated to true. Basically, it allows you to -build a new array of values based on some search criteria. -If we wanted to find all products whose type was “E-Commerce” -we could do something like the code below. -
-Note that even if only one match is made, just as in this case, -the result is still returned as an array. In that case, -ecom.company would return undefined.
- -Unlike the findAll function, the detect function will only -return the first item for which the expression inside -the iterator is true. So, if we wanted to find the first number that -was greater than 5 we’d do something like the code below. -
-Even though, there are other numbers above 5 in our array, detect -only gives us the first match back.
- -The invoke function allows us to pass a method as a string and -have that method invoked. For instance, if we wanted to sort -our array of artists we’d do something like this:
- -Why not just use F.Artists.sort? Well, for the example above -we could do just that, but here is where invoke shines.
- -So we invoked sort for each sub-array. Note that the code below will not work.
- -The reason this will not work is because it is taking each item -in that array and trying to apply sort to it, thus if we wrote it outright, -it would look something like this:
- -We could however do something like this:
- --Now, what about passing arguments to the invoke function? -The first argument passed to invoke is the method to be invoked, -and any other arguments beyond that will be passed as arguments to the invoked method.
- -This tutorial introduces the Prado web application framework's + ActiveRecord + and Active Controls to build a Chat + web application . It is assumed that you + are familiar with PHP and you have access to a web server that is able to serve PHP5 scripts. + This basic chat application will utilizing the following ideas/components in Prado. +
In this tutorial you will build an AJAX Chat web application that allows
+ multiple users to communicate through their web browser.
+ The application consists of two pages: a login page
+ that asks the user to enter their nickname and the main application chat
+ page.
+ You can try the application locally or at
+ Pradosoft.com.
+ The main application chat page is shown bellow.
+ class="figure" />
+
The download and installation steps are similar to those in
+ the Currency converter tutorial.
+ To create the application, we run from the command line the following.
+ See the Command Line Tool
+ for more details.
+
The above command creates the necessary directory structure and minimal + files (including "index.php" and "Home.page") to run a Prado web application. + Now you can point your browser's url to the web server to serve up + the index.php script in the chat directory. + You should see the message "Welcome to Prado!" +
+ +The first task for this application is to ensure that each user + of the chat application is assigned with a unique (choosen by the user) + username. To achieve this, we can secure the main chat application + page to deny access to anonymouse users. First, let us create the Login + page with the following code. We save the Login.php and Login.page + in the chat/protected/pages/ directory (there should be a Home.page + file there create by the command line tool). +
+The login page contains
+ a class="figure" />
+ If you click on the Login button without entering any
+ text in the username textbox, an error message is displayed. This is
+ due to the
Now we wish that if the user is trying to access the main application
+page, Home.page, before they have logged in, the user is presented with
+the Login.page first. We add a chat/protected/application.xml configuration
+file to import some classes that we shall use later.
+
If you now try to access the Home page by pointing your browser +to the index.php you will be redirected to the Login page. +
+ +The
To implement a custom user manager module class we just need +to extends the TModule class and implement the IUserManager +interface. The getGuestName(), getUser() and validateUser +methods are required by the IUserManager interface. +We save the custom user manager class as App_Code/ChatUserManager.php. +
++The getGuestName() +method simply returns the name for a guest user and is not used in our application. +The getUser() method returns a TUser object if the username +exists in the database, the TUser object is set with role of "normal" +that corresponds to the <authorization> rules defined in our +config.xml file.
+ +The addNewUser() and usernameExists() +method uses the ActiveRecord corresponding to the chat_users table to +add a new user and to check if a username already exists, respectively. +
+ +The next thing to do is change the config.xml configuration to use +our new custom user manager class. We simply change the <module> +configuration with id="users".
+To perform authentication, we just want the user to enter a unique
+username. We add a
+
+In the createNewUser method, when the validation passes (that is, +when the user name is not taken) we add a new user. Afterward we perform +a manual login process: +
If you try to perform a login now, you will receive an error message like
+"Property 'ChatUserRecord::$last_activity' must not be null as defined
+by column 'last_activity' in table 'chat_users'.". This means that the $last_activity
+property value was null when we tried to insert a new record. We need to either
+define a default value in the corresponding column in the table and allow null values or set the default
+value in the ChatUserRecord class. We shall demonstrate the later by
+altering the ChatUserRecord with the addition of a set getter/setter
+methods for the last_activity property.
+
+
Now we are ready to build the main chat application. We use a simple
+layout that consist of one panel holding the chat messages, one panel
+to hold the users list, a textare for the user to enter the text message
+and a button to send the message.
+Prado Chat Demo
+
+
Lets have some fun before we proceed with setuping the chat buffering. We want
+to see how we can update the current page when we receive a message. First, we add
+an OnClick event handler for the Send button.
+
+
To append or add some content to the message list panel, we need to use
+some methods in the
+
To send a message to all the connected users we need to buffer or store
+the message for each user. We can use the database to buffer the messages. The
+chat_buffer table is defined as follows.
+
We finally arrive at the guts of the chat application logic. First, we
+need to save a received message into the chat buffer for all the
+current users. We add this logic in the ChatBufferRecord class.
+
+
The next piece of the logic is to retreive the users messages from the buffer. +We simply load all the messages for a particular username and format that message +appropriately (remember to escape the output to prevent Cross-Site Scripting attacks). +After we load the messages, we delete those loaded messages and any older +messages that may have been left in the buffer. +
+Nows comes to put the application flow together. In the Home.php we update
+the Send buttons OnClick event handler to use the application
+logic we just implemented.
+
At this point the application is actually already functional, just not very +user friendly. If you open two different browser, you should be able to communicate +between the two users when ever the Send button is clicked. +
+ +The next part is perphaps the more tricker and fiddly than the other tasks. We
+need to improve the user experience. First, we want a list of current users
+as well. So we add the following method to Home.php, we can call
+this method when ever some callback event is raised, e.g. when the Send
+button is clicked.
+
Actually, we want to periodically update the messages and user list as new +users join in and new message may arrive from other users. So we need to refresh +the message list as well.
+Next, we need to redirect the user back to the login page if the user has
+been inactive for some time, say about 5 mins, we can add this check to any stage
+of the page life-cycle. Lets add it to the onLoad() stage.
+
The last few details are to periodically check for new messages and
+refresh the user list. We can accomplish this by polling the server using a
+
The final piece requires us to use some javascript. We want that when the
+user type some text in the textarea and press the Enter key, we want it
+to send the message without clicking on the Send button. We add to the
+Home.page some javascript.
+
+
This completes the tutorial on making a basic chat web application using +the Prado framework. Hope you have enjoyed it. +
+ +To install Prado, simply download the latest version of Prado from
http://www.pradosoft.com
and unzip the file to a directory not accessible by your web server
diff --git a/demos/quickstart/protected/pages/Tutorial/chat1.png b/demos/quickstart/protected/pages/Tutorial/chat1.png
new file mode 100644
index 00000000..8288b496
Binary files /dev/null and b/demos/quickstart/protected/pages/Tutorial/chat1.png differ
diff --git a/demos/quickstart/protected/pages/Tutorial/chat2.png b/demos/quickstart/protected/pages/Tutorial/chat2.png
new file mode 100644
index 00000000..97cbc51d
Binary files /dev/null and b/demos/quickstart/protected/pages/Tutorial/chat2.png differ
diff --git a/framework/Data/ActiveRecord/Exceptions/messages.txt b/framework/Data/ActiveRecord/Exceptions/messages.txt
index f77b2275..92bdb30f 100644
--- a/framework/Data/ActiveRecord/Exceptions/messages.txt
+++ b/framework/Data/ActiveRecord/Exceptions/messages.txt
@@ -9,4 +9,5 @@ ar_no_primary_key_found = Table '{0}' does not contain any primary key fiel
ar_primary_key_is_scalar = Primary key '{1}' in table '{0}' is NOT a composite key, invalid value '{2} used.
ar_invalid_db_connection = Missing or invalid default database connection for ActiveRecord class '{0}', default connection is set by the DbConnection property of TActiveRecordManager.
ar_mismatch_args_exception = ActiveRecord finder method '{0}' expects {1} parameters but found only {2} parameters instead.
-ar_invalid_tablename_property = ActiveRecord tablename property '{0}::${1}' must be static and not null.
\ No newline at end of file
+ar_invalid_tablename_property = ActiveRecord tablename property '{0}::${1}' must be static and not null.
+ar_value_must_not_be_null = Property '{0}::${2}' must not be null as defined by column '{2}' in table '{1}'.
\ No newline at end of file
diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php
index 0855fabf..68d63a23 100644
--- a/framework/Data/ActiveRecord/TActiveRecord.php
+++ b/framework/Data/ActiveRecord/TActiveRecord.php
@@ -210,6 +210,28 @@ abstract class TActiveRecord extends TComponent
return $gateway->deleteRecordsByPk($this,(array)$keys);
}
+
+ /**
+ * Delete multiple records using a criteria.
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return int number of records deleted.
+ */
+ public function deleteAll($criteria, $parameters=array())
+ {
+ if(is_string($criteria))
+ {
+ if(!is_array($parameters) && func_num_args() > 1)
+ {
+ $parameters = func_get_args();
+ array_shift($parameters);
+ }
+ $criteria=new TActiveRecordCriteria($criteria,$parameters);
+ }
+ $gateway = $this->getRecordManager()->getRecordGateway();
+ return $gateway->deleteRecordsByCriteria($this, $criteria);
+ }
+
/**
* Populate the record with data, registers the object as clean.
* @param string new record name
diff --git a/framework/Data/ActiveRecord/TActiveRecordGateway.php b/framework/Data/ActiveRecord/TActiveRecordGateway.php
index e7ea5e46..1cb1c79f 100644
--- a/framework/Data/ActiveRecord/TActiveRecordGateway.php
+++ b/framework/Data/ActiveRecord/TActiveRecordGateway.php
@@ -246,6 +246,20 @@ class TActiveRecordGateway extends TComponent
return $command->execute();
}
+ /**
+ * Delete multiple records by criteria.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria search criteria
+ * @return int number of records.
+ */
+ public function deleteRecordsByCriteria(TActiveRecord $record, $criteria)
+ {
+ $meta = $this->getMetaData($record);
+ $command = $meta->getDeleteByCriteriaCommand($record->getDBConnection(),$criteria);
+ $this->raiseCommandEvent(TActiveRecordStatementType::Delete,$command,$record,$criteria);
+ return $command->execute();
+ }
+
/**
* Raise the corresponding command event, insert, update, delete or select.
* @param string command type
diff --git a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
index 74c97689..fffdb6fb 100644
--- a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
+++ b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
@@ -161,9 +161,9 @@ abstract class TDbMetaDataCommon extends TDbMetaData
{
$conn->setActive(true);
$numKeys = count($this->getPrimaryKeys());
- if($numKeys===0)
- throw new TActiveRecordException('ar_no_primary_key_found',$this->getTableName());
$table = $this->getTableName();
+ if($numKeys===0)
+ throw new TActiveRecordException('ar_no_primary_key_found',$table);
if($numKeys===1)
$criteria = $this->getDeleteInPkCriteria($conn,$keys);
else
@@ -173,6 +173,21 @@ abstract class TDbMetaDataCommon extends TDbMetaData
$command->prepare();
return $command;
}
+
+
+ /**
+ * SQL command to delete records by criteria
+ * @param TDbConnection database connection.
+ * @param TActiveRecordCriteria criteria object.
+ * @return TDbCommand delete command.
+ */
+ public function getDeleteByCriteriaCommand($conn, $criteria)
+ {
+ $conditions = $criteria!==null?$this->getSqlFromCriteria($conn,$criteria) : '';
+ $table = $this->getTableName();
+ $sql = "DELETE FROM {$table} {$conditions}";
+ return $this->createCriteriaBindedCommand($conn,$sql, $criteria);
+ }
}
?>
\ No newline at end of file
diff --git a/framework/Data/TDataSourceConfig.php b/framework/Data/TDataSourceConfig.php
index 2cf74106..93857b16 100644
--- a/framework/Data/TDataSourceConfig.php
+++ b/framework/Data/TDataSourceConfig.php
@@ -101,6 +101,15 @@ class TDataSourceConfig extends TModule
return $this->_conn;
}
+ /**
+ * Alias for getDbConnection().
+ * @return TDbConnection database connection.
+ */
+ public function getDatabase()
+ {
+ return $this->getDbConnection();
+ }
+
/**
* @param string Database connection class name to be created.
*/
diff --git a/framework/Web/UI/ActiveControls/TCallbackClientScript.php b/framework/Web/UI/ActiveControls/TCallbackClientScript.php
index 19e395df..b10552e8 100644
--- a/framework/Web/UI/ActiveControls/TCallbackClientScript.php
+++ b/framework/Web/UI/ActiveControls/TCallbackClientScript.php
@@ -274,6 +274,15 @@ class TCallbackClientScript extends TApplicationComponent
$this->callClientFunction('Element.scrollTo', $element);
}
+ /**
+ * Focus on a particular element.
+ * @param TControl control element or element id.
+ */
+ public function focus($element)
+ {
+ $this->callClientFunction('Prado.Element.focus', $element);
+ }
+
/**
* Sets the style of element. The style must be a key-value array where the
* key is the style property and the value is the style value.
diff --git a/framework/Web/UI/TPage.php b/framework/Web/UI/TPage.php
index dc81ccbb..53c9b03f 100644
--- a/framework/Web/UI/TPage.php
+++ b/framework/Web/UI/TPage.php
@@ -340,7 +340,10 @@ class TPage extends TTemplateControl
*/
public function getCallbackClient()
{
- return $this->getAdapter()->getCallbackClientHandler();
+ if($this->getAdapter() !== null)
+ return $this->getAdapter()->getCallbackClientHandler();
+ else
+ return new TCallbackClientScript();
}
/**
diff --git a/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page
new file mode 100644
index 00000000..acf56bfe
--- /dev/null
+++ b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page
@@ -0,0 +1,27 @@
+
+
+
+