Yii DataProvider change the attributes of a column in each result - php

I'm using Yii's Dataprovider to output a bunch of users based on the column "points";
It works fine now but I have to add a feature so if the user is online, he gets an extra 300 points.
Say Jack has 100 points, Richmond has 300 points, However Jack is online, so Jack should rank higher than Richmond.
Here is my solution now:
$user=new Rank('search');
$user->unsetAttributes();
$user->category_id = $cid;
$dataProvider = $user->search();
$iterator = new CDataProviderIterator($dataProvider);
foreach($iterator as $data) {
//check if online ,update points
}
However, this CDataProviderIterator seems change my pagination directly to the last page and I can't even switch page anymore. What should I do?
Thank you very much!
Here is the listview:
$this->widget('zii.widgets.CListView', array(
'id'=>'userslist',
'dataProvider'=>$dataProvider,
'itemView'=>'_find',
'ajaxUpdate'=>false,
'template'=>'{items}<div class="clear"></div><div style="margin-right:10px;"><br /><br />{pager}</div>',
'pagerCssClass'=>'right',
'sortableAttributes'=>array(
// 'create_time'=>'Time',
),
));
Updated codes in Rank.php model
$criteria->with = array('user');
$criteria->select = '*, (IF(user.lastaction > CURRENT_TIMESTAMP() - 1800, points+300, points)) as real_points';
$criteria->order = 'real_points DESC';
However, it throws me error:
Active record "Rank" is trying to select an invalid column "(IF(user.lastaction > CURRENT_TIMESTAMP() - 1800". Note, the column must exist in the table or be an expression with alias.

CDataProviderIterator iterates every dataprovider value, and stops at the end. I don't know all about this classes, but think the reason is in some internal iterator, that stops at the end of dataprovider after your foreach.
Iterators are used when you need not load all data (for large amounts of data) but need to process each row.
To solve your problem, just process data in your view "_find". Add points there if online.
Or if you want place this logic only in the model (following MVC :) ), add method to your model:
public function getRealPoints() {
return ($this->online) ? ($this->points + 300) : $this->points;
}
And you can use $user->realPoints to get points according to user online status
update: To order your list by "realPoints" you need to get it in your SQL.
So use your code:
$user=new Rank('search');
$user->unsetAttributes();
$user->category_id = $cid;
$dataProvider = $user->search();
and modify $user->search() function, by adding:
$criteria->select = '*, (IF(online='1', points+300, points)) as real_points';
$criteria->order = 'real_points DESC';
where online and points - your table columns.

Related

display data for every n-th data using clistview

how can i display data for every let's say odd entry?
So first record Z, second record do Y, third record do Z, fourth do Y.
or lets say i get from model every nth record, and another that does n-1?
public function getNewItem()//for main page grids
{
$criteria = new CDbCriteria;
$criteria->order = 'posted_date DESC';
$criteria->compare('product_status',"Y");
$criteria->compare('product_approval_status',"Y");
//????
return new CActiveDataProvider( $this, array(
'criteria'=>$criteria,
'pagination'=>false,
));
}
There are two ways to do this; depending on how complex your filtering needs to be;
First method you can modify your criteria compare to filter by only odd or even records like this
$criteria = new CDbCriteria;
$criteria->order = 'posted_date DESC';
$criteria->compare('product_status',"Y");
$criteria->compare('product_approval_status',"Y");
$criteria->addCondition('MOD(id,2)=1','AND');
or any such condition to get the corresponding subset of records
Second method is to filter it in the partial file you use in the CListView, I use this method to usually add additional code not for filtering, for example you want to add a new row every forth element, For this use the $indexand $widget variable made available in the itemView attribute in the CListView call with something like this.
<?php if(($index)%6 == 0 || $index == 0 ){ ?>
<div class="row">
<?php } ?>
// Regular view rendering code
// ...
<?php if(($index+1)%6 == 0 || ($index+1) == $widget->dataProvider->itemCount){ ?>
</div>
<?php }
Here I have used this to insert a new div for every 6 items in the list or if its the first/last item
This is useful for conditional rendering of items/ adding classes etc.

How to interact with preview and save using flourishlib's fRecordSet's build method?

Let's suppose I have a database table called local_ads.
Now, when a local ad is created, one has to be able to view its preview and if he is satisfied, then save it. Also, if one wants to update a local ad, then he might want to see its preview before he overwrites the live version of the record.
So, I have a foreign key for the local_ads table called parent_id. If this is null, then it is a preview (at least according to my initial thoughts). Otherwise it is live. When one saves a preview, there are two cases:
Case 1: There was no live record yet linked to the preview. In this case a new record is inserted into the local_ads table with the parent_id pointing to the preview.
Case 2: There is a live record linked to the preview. In this case the live record is updated.
Everything looks and works nicely, but I have a problem showing the results in a grid. I want to show the preview if no live version of the record exists and only the live version if it exists. I would like to show something in the spirit of
select col1, col2, col3, col4
from local_ads glob
where (not (parent_id is null))
or ((select id from local_ads temp where temp.parent_id = glob.id limit 0, 1) is null)
but I have several problems. We have a logical or (I wonder how can I use logical or between logical operands using the build method of fRecordSet of flourishlib). Also, this query is of two dimensions, it is slow. Also, I wonder how can a sub-query be executed. Also, I do not know how can I use the is operator as in is null.
So, I had to re-think my idea and I came up with the following:
select col1, col2, col3, col4
from local_ads
where parent_id < id or parent_id >= id
the idea is simple: If a preview does not have a live version, then parent_id matches the id, otherwise the parent_id of a preview is null. I know this is an ugly hack, but this was the best idea I could came up with to solve the problem and decrease memory and performance complexity.
So, the only problem which remained was to check the two logical values in the where clause separated by the logical or.
From the documentation I have seen this:
* 'column<:' => 'other_column' // column < other_column
and this:
* 'column>=:' => 'other_column' // column >= other_column
so I know how can I add these to the filters, but how am I supposed to 'or' them?
So far I have tried it this way:
public static function localAd() {
$User = Globe::load('CurrentUser');
$Smarty = Globe::load('Smarty');
//handle default options
$options = array(
'recordsPerPage' => 20,
'pageLinks' => 10,
);
$page = 0;
if (isset($_GET['p'])) {
$page = $_GET['p'];
}
//get the data
$startIndex = (isset($page)) ? $page * $options['recordsPerPage'] : 0;
$filters = array();
if ($User->getType() == 'local_admin') {
$filters['domain='] = $User->getDomain();
}
$records = fRecordSet::build('LocalAd', $filters, array('created' => 'desc'), $options['recordsPerPage'], $page + 1);
//create result object for pagination
$Result = array(
"recordsReturned" => $records->count(),
"totalRecords" => $records->count(true),
"startIndex" => intval($startIndex),
"records" => $records->export(),
'recordsPerPage' => $options['recordsPerPage'],
'pageLinks' => $options['pageLinks'],
'currentPage' => $page,
//'options' => $options
);
$Result['totalPages'] = ceil($Result['totalRecords'] / $Result['recordsPerPage']);
$Smarty->assign('Result', $Result);
$Smarty->assign('ManagerURL', '?a=localAd');
AdminView::display('Admin/LocalAd/main.tpl');
}
Note, that in some cases I have to check the domain as well.
In the meantime I have managed to solve the problem. This is how we can define the filter set to solve the problem mentioned in the question:
$filters = array();
if ($User->getType() == 'local_admin') {
$filters['domain='] = $User->getDomain();
}
$filters['parent_id<:|parent_id>=:'] = array('id', 'id');

CakePHP & MVC – Potentially superfluous SQL queries when looking up 'names' associated with ids

I've probably murdered the whole concept of MVC somewhere along the line, but my current situation is thus:
I have participants in events and a HABTM relationship between them (with an associated field money_raised). I have a controller that successfully creates new HABTM relationships between pre-existing events and participants which works exactly as I want it to.
When a new relationship is created I wish to set the flash to include the name of the participant that has just been added. The actually addition is done using ids, so I've used the following code:
public function addParticipantToEvent($id = null) {
$this->set('eventId', $id);
if ($this->request->is('post')) {
if ($this->EventsParticipant->save($this->request->data)) {
$participant_id = $this->request->data['EventsParticipant']['participant_id'];
$money_raised = $this->request->data['EventsParticipant']['money_raised'];
$participant_array = $this->EventsParticipant->Participant->findById($participant_id);
$participant_name = $participant_array['Participant']['name'];
$this->Session->setFlash('New participant successfully added: ' . $participant_name . ' (' . $participant_id . ') ' . '— £' . $money_raised);
} else {
$this->Session->setFlash('Unable to create your event-participant link.');
}
}
}
This works, but generates the following SQL queries:
INSERT INTO `cakephptest`.`cakephptest_events_participants` (`event_id`, `participant_id`, `money_raised`) VALUES (78, 'crsid01', 1024) 1 1 0
SELECT `Participant`.`id`, `Participant`.`name`, `Participant`.`college` FROM `cakephptest`.`cakephptest_participants` AS `Participant` WHERE `Participant`.`id` = 'crsid01' LIMIT 1 1 1 0
SELECT `Event`.`id`, `Event`.`title`, `Event`.`date`, `EventsParticipant`.`id`, `EventsParticipant`.`event_id`, `EventsParticipant`.`participant_id`, `EventsParticipant`.`money_raised` FROM `cakephptest`.`cakephptest_events` AS `Event` JOIN `cakephptest`.`cakephptest_events_participants` AS `EventsParticipant` ON (`EventsParticipant`.`participant_id` = 'crsid01' AND `EventsParticipant`.`event_id` = `Event`.`id`)
This final one seems superfluous (and rather costly) as the second should give me all that I need, but removing $this->EventsParticipant->Participant->findById($participant_id) takes out both the second and third queries (which sort of makes sense to me, but not fully).
What can I do to remedy this redundancy (if indeed I'm not wrong that it is a redundancy)? Please tell me if I've made a complete hash of how these sorts of things should work – I'm very new to this.
This is probably due to the default recursive setting pulling the relationship. You can remedy this by setting public $recursive = -1; on your model (beware this will affect all find calls). Or, disable it temporarily for this find:
$this->EventsParticipant->Participant->recursive = -1;
$this->EventsParticipant->Participant->findById($participant_id);
I always suggest setting public $recursive = -1; on your AppModel and using Containable to bring in the related data where you need it.

Complex Zend Query from same user table

I have a rather unique set of conditions and orders in which I need to retrieve data from a "sellers" table for an application I'm building in Zend framework.
The client is basically requesting an application where the directory page lists sellers in a very particular order, which is:
Sellers who have been approved in the last 7 days (then order by #4 below)
Then, selllers who have paid for upgraded features on the site, and are more the 7 days old (then order by #4 below)
Then, Sellers who are more than 7 days old and are more than 7 days old (then order by #4 below)
For all of the above, secondary order by would be their launch date, then alpha by business name
I'm trying to figure out the most effective way to write an action helper that will return the data in the correct sequence above, knowing that some of my views only need 1,2 (and 4), whereas other views within the application will need all 4.
Right now, I've been writing two or three separate queries, and passing them to 2 or 3 partialloop's inside the view, but I strive for properly written code, and would like to either combine my 3 queries into one object I can pass to one partial loop, or.... write one query. How can this be done?
Here's my helper at the moment:
class Plugin_Controller_Action_Helper_ListSellers extends Zend_Controller_Action_Helper_Abstract
{
//put your code here
public function direct($regulars = false, $filter = false)
{
$dateMod = $this->dateMod = new DateTime();
$dateMod->modify('-7 days');
$formattedDate = $dateMod->format('Y-m-d H:i:s');
// get sellers initialized in last 7 days
$sellerTable = new Application_Model_DbTable_Seller();
// get sellers initialized in last 7 days
$select = $sellerTable->select()->setIntegrityCheck(false);
$select->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$select->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$select->order('s.launchDate DESC','s.businessName ASC');
$select->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1');
$select->where('s.launchDate > ?', $formattedDate);
if($filter){ $select->where('s.categoryID = ?', $filter);}
$newSellers = $sellerTable->fetchAll($select);
$query = $sellerTable->select()->setIntegrityCheck(false);
$query->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$query->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$query->order('s.launchDate DESC','s.businessName ASC');
$query->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1 AND s.featured = 1');
$query->where('s.launchDate < ?', $formattedDate);
if($filter){ $select->where('s.categoryID = ?', $filter);}
$featuredSellers = $sellerTable->fetchAll($query);
if($regulars){
$where = $sellerTable->select()->setIntegrityCheck(false);
$where->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$where->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$where->order('s.launchDate DESC','s.businessName ASC');
$where->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1 AND s.featured IS NULL');
$where->where('s.launchDate < ?', $formattedDate);
$regularSellers = $sellerTable->fetchAll($where);
}
}
}
I don't see any limits being applied to your queries. So does that mean you really want to select all matching records? For scalability reasons I'd guess that the answer should be no, there will be limits applied. In this case, you may just have to do 3 different queries.
But if there are no limits to be applied, then you could do a single simple query that selects all sellers, unfiltered and unsorted, and do your sorting and filtering in view helpers or just in your views.
Regardless, I recommend not putting database queries inside your controller layer, assuming you want to use the Model-View-Controller pattern which Zend is built for. Controllers should be thin. Your models should handle all database queries and just spit out the results into your controllers. I use the Data Mapper pattern extensively. Something like:
$mapper = new Application_Model_SellerMapper();
$newSellers = $mapper->fetchNewSellers();
$featuredSellers = $mapper->fetchFeaturedSellers();
$regularSellers = $mapper->fetchRegularSellers();
Each of your fetchX() methods would return an array of Application_Model_Seller instances, rather than Zend_Db_Table_Row instances.
This way you maintain Separation of Concerns and Single Responsibility Principle better, for more maintainable code. Even if you're the only developer on the project over the long-term, 6 months from now you won't remember what you wrote and why. And if someone else comes on the project, clarity becomes really important.

zend framework paginator (Zend_Paginator) results too slow

I have a query that running way too slow. the page takes a few minutes to load.
I'm doing a table join on tables with over 100,000 records. In my query, is it grabbing all the records or is it getting only the amount I need for the page? Do I need to put a limit in the query? If I do, won't that give the paginator the wrong record count?
$paymentsTable = new Donations_Model_Payments();
$select = $paymentsTable->select(Zend_Db_Table::SELECT_WITH_FROM_PART);
$select->setIntegrityCheck(false)
->from(array('p' => 'tbl_payments'), array('clientid', 'contactid', 'amount'))
->where('p.clientid = ?', $_SESSION['clientinfo']['id'])
->where('p.dt_added BETWEEN \''.$this->datesArr['dateStartUnix'].'\' AND \''.$this->datesArr['dateEndUnix'].'\'')
->join(array('c' => 'contacts'), 'c.id = p.contactid', array('fname', 'mname', 'lname'))
->group('p.id')
->order($sortby.' '.$dir)
;
$payments=$paymentsTable->fetchAll($select);
// paginator
$paginator = Zend_Paginator::factory($payments);
$paginator->setCurrentPageNumber($this->_getParam('page'), 1);
$paginator->setItemCountPerPage('100'); // items pre page
$this->view->paginator = $paginator;
$payments=$payments->toArray();
$this->view->payments=$payments;
Please see revised code below. You need to pass the $select to Zend_Paginator via the correct adapter. Otherwise you won't see the performance benefits.
$paymentsTable = new Donations_Model_Payments();
$select = $paymentsTable->select(Zend_Db_Table::SELECT_WITH_FROM_PART);
$select->setIntegrityCheck(false)
->joinLeft('contacts', 'tbl_payments.contactid = contacts.id')
->where('tbl_payments.clientid = 39')
->where(new Zend_Db_Expr('tbl_payments.dt_added BETWEEN "1262500129" AND "1265579129"'))
->group('tbl_payments.id')
->order('tbl_payments.dt_added DESC');
// paginator
$paginator = new Zend_Paginator(new Zend_Paginator_Adapter_DbTableSelect($select));
$paginator->setCurrentPageNumber($this->_getParam('page', 1));
$paginator->setItemCountPerPage('100'); // items pre page
$this->view->paginator = $paginator;
Please see revised code above!
In your code, you are :
first, selecting and fetching all records that match your condition
see the select ... from... and all that
and the call to fetchAll on the line just after
and, only the, you are using the paginator,
on the results returned by the fetchAll call.
With that, I'd say that, yes, all your 100,000 records are fetched from the DB, manipulated by PHP, passed to Zend_Paginator which has to work with them... only to discard almost all of them.
Using Zend_Paginator, you should be able to pass it an instance of Zend_Db_Select, and let it execute the query, specifying the required limit.
Maybe the example about DbSelect and DbTableSelect adapter might help you understand how this can be achieved (sorry, I don't have any working example).
I personally count the results via COUNT(*) and pass that to zend_paginator. I never understood why you'd deep link zend_paginator right into the database results. I can see the pluses and minuses, but really, its to far imho.
Bearing in mind that you only want 100 results, you're fetching 100'000+ and then zend_paginator is throwing them away. Realistically you want to just give it a count.
$items = Eurocreme_Model::load_by_type(array('type' => 'list', 'from' => $from, 'to' => MODEL_PER_PAGE, 'order' => 'd.id ASC'));
$count = Eurocreme_Model::load_by_type(array('type' => 'list', 'from' => 0, 'to' => COUNT_HIGH, 'count' => 1));
$paginator = Zend_Paginator::factory($count);
$paginator->setItemCountPerPage(MODEL_PER_PAGE);
$paginator->setCurrentPageNumber($page);
$this->view->paginator = $paginator;
$this->view->items = $items;

Categories