/[cvs]/nfo/php/libs/com.newsblob.phphtmllib/widgets/data_list/DataList.inc
ViewVC logotype

Annotation of /nfo/php/libs/com.newsblob.phphtmllib/widgets/data_list/DataList.inc

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.1 - (hide annotations)
Thu Jan 30 03:29:45 2003 UTC (21 years, 6 months ago) by jonen
Branch: MAIN
Branch point for: no_vendor_tag
Initial revision

1 jonen 1.1 <?php
2    
3     /**
4     * This is the base class for managing a list
5     * of data points.
6     *
7     * @author Walter A. Boring IV <waboring@buildabetterweb.com>
8     * @package phpHtmlLib
9     */
10    
11     define("SORTABLE", TRUE);
12     define("NOT_SORTABLE", FALSE);
13     define("SEARCHABLE", TRUE);
14     define("NOT_SEARCHABLE", FALSE);
15    
16     //Some defines for setting up the search
17     //modifier fields.
18     define("SEARCH_BEGINS_WITH", 1);
19     define("SEARCH_CONTAINS", 2);
20     define("SEARCH_EXACT", 4);
21     define("SEARCH_ENDS_WITH", 8);
22     define("SEARCH_ALL", 15);
23    
24    
25     /**
26     * This object is the base class that can be
27     * used to build a widget that shows lists of
28     * data from any source via the DataListSource
29     * object. It fetches/builds/gets its data
30     * from the DataListSource object, which can
31     * be written to support any data source
32     * (MySQL, Oracle, comma delimited file, xml, etc.)
33     *
34     * This base class MUST be extended by a child to
35     * actually render the list. The job of the DataList
36     * class is to provide the basic API and abstraction
37     * mechanism to handling searching, showing, sorting
38     * lists of data.
39     *
40     * Each column of data is associated with a title and a
41     * name. The title is what is shown to the user for that
42     * column. The name is the mapping between the column and
43     * the DataListSource. Each column can be marked
44     * as sortable and searchable. If a column is sortable,
45     * the title is a link that can be clicked on to sort.
46     * The sorting is done in the DataListSource object (
47     * via the sql query, or sort() functions depending on the
48     * data source itself)
49     *
50     * The DataList object will build the title, the search block
51     * (if any), the datalist controls (the links/imges for
52     * first, prev, next, last, all). Then the data columns/labels.
53     * Then it will fetch each of the rows of data to display
54     * from the DataListSource.
55     *
56     * The logic of the output calls follows in the order:
57     *
58     * title
59     * search table/block
60     * datalist controls (first, prev, next, last, all)
61     * data columns/labels
62     * data rows x through y
63     *
64     *
65     * REQUIREMENTS:
66     * You must use/define a DataListSource object.
67     * You MUST override/extend the following methods:
68     *
69     * * get_data_source() - used to set the DataListSource
70     * by this class.
71     * * user_setup() - used to set the columns to show and any
72     * options used by the DataList class and
73     * DataListSource object.
74     *
75     *
76     * UI ABSTRACTION METHODS
77     * These methods allow for some level of abstraction for
78     * the layout/look/feel of the actual list of data.
79     *
80     *
81     * * gui_init() - the function gives the child class a chance
82     * to do any building of the objects that will
83     * hold the search area, column headers and the
84     * rows of data.
85     *
86     * * child_build_column_header() - This method is responsible
87     * for building and inserting
88     * the column header title into
89     * the UI object.
90     *
91     * * child_add_row_cell() - This function is responsible for adding
92     * the cell in the current row. The method
93     * is responsible for keeping track of the
94     * location in its UI object for the current
95     * row.
96     *
97     * * child_get_gui() - This method returns the entire UI in 1 object
98     * or container. At this point the entire UI
99     * has been constructed, the entire list of data
100     * has been walked and inserted.
101     *
102     *
103     * @author Walter A. Boring IV <waboring@buildabetterweb.com>
104     * @package phpHtmlLib
105     */
106     class DataList extends BaseWidget {
107    
108     /**
109     * This value holds the number
110     * of pages of data we have
111     * to display.
112     *
113     */
114     var $_num_pages=1;
115    
116     /**
117     * The number of rows of data
118     * to show per "page".
119     * The default is 20.
120     *
121     */
122     var $_default_rows_per_page = 10;
123    
124     /**
125     * The max number of rows to
126     * show when the user does the
127     * "EXPAND" command.
128     */
129     var $_max_rows = 200;
130    
131    
132     /**
133     * Do we want to alternate the row colors?
134     * This helps to see each row easier.
135     */
136     var $alternating_row_colors = TRUE;
137    
138     /**
139     * prefix for all list variable
140     * names, so we can potentially
141     * have more then 1 list per page.
142     */
143     var $_global_prefix = '';
144    
145     /**
146     * The offset variable name
147     */
148     var $_offsetVar = "offset";
149    
150     /**
151     * The order by variable name
152     */
153     var $_orderbyVar = "orderby";
154    
155     /**
156     * Holds the db column name that
157     * we want to order by default.
158     */
159     var $_default_orderby = '';
160    
161     /**
162     * The reverseorder variable name
163     */
164     var $_reverseorderVar = "reverseorder";
165    
166     /**
167     * Holds a flag to let us know to
168     * reverse order the column by default
169     */
170     var $_default_reverseorder = "false";
171    
172     /**
173     * The number of rows variable name
174     */
175     var $_numrowsVar = "numrows";
176    
177     /**
178     * This var to tell us to show all
179     * data or not.
180     */
181     var $_showallVar = "showall";
182    
183     /**
184     * The search field variable
185     */
186     var $_search_fieldVar = "search_field";
187    
188     /**
189     * The search value
190     */
191     var $_search_valueVar = "search_value";
192    
193     /**
194     * The type of search
195     * advanced/simple
196     */
197     var $_search_typeVar = "search_type";
198    
199     /**
200     * The simple search modifier
201     * var name.
202     *
203     */
204     var $_simple_search_modifierVar = "simple_search_modifier";
205    
206    
207     /**
208     * Flag to let us know that search
209     * is enabled.
210     *
211     */
212     var $_search_flag = FALSE;
213    
214     /**
215     * Flag to let us know that
216     * advanced search is enabled
217     *
218     */
219     var $_advanced_search_flag = FALSE;
220    
221     /**
222     * Flag to enable simple search modifyer.
223     * IF enabled it will add a select that adds
224     * the "beings with", "contains" options for
225     * a simple search.
226     */
227     var $_simple_search_modifier = FALSE;
228    
229    
230     /**
231     * Holds the object block that is the
232     * search UI
233     */
234     var $_search_table = NULL;
235    
236     /**
237     * This holds a list of
238     * name=>value vars that the
239     * caller/child wants to propogate
240     * automatically.
241     *
242     */
243     var $_save_vars = array();
244    
245    
246     /**
247     * The column descriptions
248     * for the data we are working on
249     *
250     * @var array
251     */
252     var $_columns = array();
253    
254     /**
255     * Keeps track of the # of columns we have
256     */
257     var $_num_columns = 0;
258    
259    
260     /**
261     * This holds the form attributes
262     *
263     */
264     var $_form_attributes = array("method" => "GET",
265     "target" => "",
266     "action" => "",
267     "name" => "");
268    
269     /**
270     * Build everything inside a form?
271     *
272     */
273     var $_form_render_flag = FALSE;
274    
275    
276     /**
277     * flag to let us know if we want to show
278     * the results or not.
279     */
280     var $_show_results_flag = TRUE;
281    
282    
283     /**
284     * Holds our reference/copy of the
285     * DataListSource object which is used to
286     * access the data that this object uses
287     *
288     * @var DataListSource object
289     */
290     var $_datasource = NULL;
291    
292     /**
293     * This stores the base path to where the
294     * tool link images live. This lets you
295     * specify a new path to where your images
296     * live.
297     */
298     var $_image_path = "/phphtmllib/images/widgets";
299    
300    
301     /**
302     * The constructor
303     *
304     * @param string - the title of the data list
305     * @param string - the overall width
306     * @param string - the column to use as the default sorting order
307     * @param boolean - sort the default column in reverse order?
308     */
309     function DataList($title, $width = "100%", $default_orderby='',
310     $default_reverseorder=FALSE) {
311     $this->set_title( $title );
312     $this->set_form_name( str_replace(' ', '_', strtolower($title)) );
313     $this->set_width( $width );
314    
315     $this->_default_orderby = $default_orderby;
316     if ($default_reverseorder === TRUE) {
317     $default_reverseorder = "true";
318     } if ($default_reverse_order === FALSE) {
319     $default_reverseorder = "false";
320     }
321     $this->_default_reverseorder = $default_reverseorder;
322    
323    
324     //Set the global prefix for our variables.
325     //want to make sure we can have multiple
326     //item lists per html page.
327     $this->set_global_prefix('');
328    
329     //child class MUST override this
330     //method to automatically set
331     //the DataListSource object.
332     //This class doesn't work without it.
333     $this->get_data_source();
334    
335     //Call the subclass setup function.
336     //This is a way to get around having
337     //to override the constructor and
338     //pass in the same params.
339     $this->user_setup();
340     }
341    
342     /**
343     * This function renders the final
344     * widget
345     *
346     */
347     function render($indent_level, $output_debug) {
348    
349     //setup the columns in their sorts
350     $this->setup_columns();
351    
352     //do any data prefetch work
353     //which may be child specific
354     if ($this->_show_results()) {
355     $this->data_prefetch();
356     }
357    
358     //This function gives the child class
359     //a chance to build the tables/divs/containers
360     //that will be responsible for the look/feel
361     //of the DataList
362     $this->gui_init();
363    
364     //see if we need to build the search area.
365     if ($this->is_search_enabled() || $this->is_advanced_search_enabled()) {
366     $this->_search_table = $this->child_build_search_table();
367     if ($this->_search_table) {
368     $this->set_form_render(TRUE);
369     }
370     }
371    
372    
373     if ($this->get_form_render()) {
374     $form = new FORMtag( array("method" => $this->get_form_method(),
375     "action" => $this->build_base_url(),
376     "name" => $this->get_form_name(),
377     "style" => "margin-top: 0px;margin-bottom:0px;") );
378    
379     $target = $this->get_form_target();
380     if ($target != NULL)
381     $form->set_tag_attribute("target",$target);
382    
383     $action = $this->get_form_action();
384     if ($action != NULL)
385     $form->set_tag_attribute("action",$action);
386    
387     //now build the UI and return it
388     $form->add( $this->build_gui() );
389    
390     //add the save vars the user wants.
391     $form->add( $this->_build_save_vars() );
392     } else {
393     $form = container();
394    
395     //now build the UI and return it
396     $form->add( $this->build_gui() );
397     }
398    
399     //add any javascript required
400     $container = container( $this->_javascript(), $form );
401     return $container->render( $indent_level, $output_debug );
402     }
403    
404     /**
405     * This function is responsible for calling the child
406     * class's methods for building the GUI container.
407     * This function builds the search area, the
408     * title, page controls, the column headers,
409     * and walks the rows of data and adds them
410     *
411     * A child class can override this method to
412     * move the placement of the search box
413     * relative to the data list. By default
414     * the search area comes above the table
415     * for the data list and page controls
416     *
417     * @return Container
418     */
419     function build_gui() {
420     $container = container();
421    
422     //if we have a search area to show
423     //add it to the ui.
424     if ($this->_search_table) {
425     $container->add( $this->_search_table );
426     }
427    
428     if ($this->_show_results()) {
429     //walk the list of columns and call the child
430     //method to add it
431     $column_count = count($this->_columns);
432     foreach( $this->_columns as $name => $col ) {
433     $this->child_build_column_header( $name, $col, $column_count);
434     }
435    
436     //now walk the list of rows and build and add the
437     //cells of data
438     $col_count = count($this->_columns);
439     while ($row = $this->_datasource->get_next_data_row()) {
440     $cnt = 1;
441     foreach( $this->_columns as $col_name => $data ) {
442     $obj = $this->build_column_item($row, $col_name);
443     if (!is_object($obj)) {
444     $obj = $this->_clean_string($obj, $col_name);
445     }
446     //
447     $this->child_add_row_cell($obj, $col_name, ($cnt == $col_count) ? TRUE : FALSE);
448     $cnt++;
449     }
450     }
451     $container->add( $this->child_get_gui() );
452     }
453    
454    
455     return $container;
456     }
457    
458     /**
459     * This function is called automatically by
460     * the DataList constructor. It must be
461     * extended by the child class to actually
462     * set the DataListSource object.
463     *
464     *
465     */
466     function get_data_source() {
467     user_error("DataList::get_data_source() - ".
468     "child class must override this to ".
469     "set the the DataListSource object.");
470     }
471    
472     /**
473     * A subclass can override this function
474     * to setup the class variables after
475     * the constructor. The constructor
476     * automatically calls this function.
477     *
478     */
479     function user_setup() {
480     user_error("DataList::user_setup() - ".
481     "child class must override this method ".
482     "to set the columns, and any options for ".
483     "the DataListSource.");
484     }
485    
486     /**
487     * A subclass can override this function
488     * to setup the class variables after
489     * the constructor. The constructor
490     * automatically calls this function.
491     *
492     */
493     function gui_init() {
494     user_error("DataList::gui_init() - ".
495     "child class must override this method ".
496     "to set up the containers/objects that will ".
497     "hold the search area, page controls, ".
498     "column headers and the data cells");
499     }
500    
501     /**
502     * This method is supposed to be written by
503     * the child class to build and add the column
504     * title to the UI
505     *
506     * @param string - the title of the column
507     * @param array - the column data ( from $this->_columns )
508     * @param int - the column #
509     *
510     */
511     function child_build_column_header($title, $col_data, $col_count) {
512     user_error("DataList::child_build_column_header() - ".
513     "child class must override this method ".
514     "to build the object that will be the ".
515     "individual column header/itle");
516     }
517    
518     /**
519     * This method is supposed to be written by
520     * the child class to add the cell data to the
521     * current row in the UI
522     *
523     * @param mixed - the object/string/entity that
524     * should get put into the column row cell.
525     * @param string - the name/title of the column that the
526     * object will live in
527     * @param boolean - flag that tells the function if this is
528     * is the last cell for the current row.
529     *
530     */
531     function child_add_row_cell($obj, $col_name, $last_in_row_flag) {
532     user_error("DataList::child_build_column_header() - ".
533     "child class must override this method ".
534     "to build the object that will be the ".
535     "individual column header/itle");
536     }
537    
538     /**
539     * This function is called after all of the data has
540     * been added to the UI object. It just returns the
541     * container that is the entire UI for the DataList
542     *
543     * @return Container
544     */
545     function child_get_gui() {
546     user_error("DataList::child_get_gui() - ".
547     "child class must override this method ".
548     "to return the wrapper that contains ".
549     "the entire UI.");
550     }
551    
552     /**
553     * This function builds the search
554     * block that lives above the results
555     *
556     * @return Container
557     */
558     function child_build_search_table() {
559     user_error("DataList::child_build_search_table() - ".
560     "child class must override this");
561     return NULL;
562     }
563    
564     /**
565     * This function provides a way to automatically
566     * add javascript to this object.
567     * This function is meant to be extended by the
568     * child class.
569     *
570     * @return SCRIPTtag object
571     */
572     function _javascript() {
573     return NULL;
574     }
575    
576     /**
577     * This function adds a header item to the column headers
578     * from a list of parameters.
579     *
580     * @param string - $label - the label to use for
581     * the column header.
582     * @param int - $size - the size for the table column.
583     * @param string - $dbfield - the db field associated
584     * with this label from the query.
585     * @param boolean - $sortable - flag to make this column sortable.
586     * @param boolean - $searchable - flag to make this column searchable.
587     * @param string - header align value.
588     * @param string - the maximum # of characters to allow in the cell.
589     *
590     * @return array a single header array
591     */
592     function add_header_item( $label, $size=100, $data_name=NULL,
593     $sortable=FALSE, $searchable=FALSE,
594     $align="left", $sortorder="",
595     $max_text_length=NULL) {
596    
597     $this->_columns[$label] = array("size" => $size,
598     "data_name" => $data_name,
599     "sortable" => $sortable,
600     "searchable" => $searchable,
601     "align" => $align,
602     "sortorder" => $sortorder,
603     "maxtextlength" => $max_text_length);
604     $this->_num_headers++;
605    
606     $this->_check_datasource("add_header_item");
607     $this->_datasource->add_column($label, $data_name, $sortable,
608     $searchable, $sortorder);
609     }
610    
611    
612     /**
613     * This function sets a prefix for all
614     * variables that are used in the item list
615     * table on a page. This allows you to have
616     * multiple itemlists on a single html page.
617     *
618     * @param string $prefix - the prefix for all vars.
619     */
620     function set_global_prefix($prefix) {
621     $this->_global_prefix = $prefix;
622     $this->_offsetVar = $prefix . "offset";
623     $this->_orderbyVar = $prefix . "orderby";
624     $this->_reverseorderVar = $prefix . "reverseorder";
625     $this->_numrowsVar = $prefix . "numrows";
626     $this->_showallVar = $prefix . "showall";
627     $this->_search_fieldVar = $prefix . "search_field";
628     $this->_search_valueVar = $prefix . "search_value";
629     $this->_search_typeVar = $prefix . $this->_search_typeVar;
630     $this->_simple_search_modifierVar = $prefix . $this->_simple_search_modifierVar;
631     }
632    
633     /**
634     * returns the current variable prefix
635     * string being used.
636     *
637     * @return string - current global prefix
638     */
639     function get_global_prefix() {
640     return $this->_global_prefix;
641     }
642    
643     /**
644     * This function is used to set the
645     * DataListSource object for this instance
646     *
647     * @param DataListSource object
648     */
649     function set_data_source( $datasource ) {
650     $this->_datasource = &$datasource;
651     }
652    
653     /**
654     * Enable the search ability.
655     *
656     * @param boolean
657     */
658     function search_enable( ) {
659     $this->_search_flag = TRUE;
660     if (!$this->is_advanced_search_enabled()) {
661     $this->set_search_type("simple");
662     }
663     }
664    
665     /**
666     * Disable the search ability.
667     *
668     * @param boolean
669     */
670     function search_disable( ) {
671     $this->_search_flag = FALSE;
672     }
673    
674     /**
675     * get the status of the search
676     * ability.
677     *
678     * @return boolean
679     */
680     function is_search_enabled() {
681     return $this->_search_flag;
682     }
683    
684    
685     /**
686     * Enable the advanced search
687     * capability
688     * NOTE: Child class MUST
689     * extend the
690     * _build_advanced_search_table
691     */
692     function advanced_search_enable() {
693     $this->_advanced_search_flag = TRUE;
694     if (!$this->is_search_enabled()) {
695     $this->set_search_type("advanced");
696     }
697     }
698    
699     /**
700     * Disable the advanced search
701     * capability
702     *
703     */
704     function advanced_search_disable() {
705     $this->_advanced_search_flag = FALSE;
706     }
707    
708     /**
709     * This returns the status of the
710     * advanced search flag.
711     *
712     * @return boolean
713     */
714     function is_advanced_search_enabled() {
715     return $this->_advanced_search_flag;
716     }
717    
718     /**
719     * Set the simple search modifyer
720     * flag.
721     * NOTE: by default all the modifiers
722     * are enabled. You can limit the
723     * modifiers by passing in the
724     * string of defines of which ones
725     * you want enabled.
726     *
727     * MODIFIERS:
728     * SEARCH_BEGINS_WITH
729     * SEARCH_CONTAINS
730     * SEARCH_EXACT
731     * SEARCH_ENDS_WITH
732     *
733     * ie. SEARCH_BEGINS_WITH.SEARCH_EXACT
734     * will enable ONLY the
735     * "begins with" and "exact" modifiers.
736     *
737     * @param string
738     */
739     function set_simple_search_modifier( $modifier = SEARCH_ALL ) {
740     if ($modifier == 0) {
741     $this->_simple_search_modifier = SEARCH_BEGINS_WITH |
742     SEARCH_CONTAINS |
743     SEARCH_EXACT |
744     SEARCH_ENDS_WITH;
745     } else {
746     $this->_simple_search_modifier = $modifier;
747     }
748     }
749    
750     /**
751     * gets the value of the search modifier
752     * flag.
753     */
754     function get_simple_search_modifier() {
755     return $this->_simple_search_modifier;
756     }
757    
758     /**
759     * This function sets the default # of rows
760     * per page to display. By default its 10.
761     * This lets the user override this value.
762     *
763     * @param int - the # of rows to use.
764     */
765     function set_default_num_rows( $num_rows ) {
766     $this->default_rows_per_page = $num_rows;
767     }
768    
769     /**
770     * This function gets the current default
771     * number of rows to display setting.
772     *
773     * @return int
774     */
775     function get_default_num_rows( ) {
776     return $this->_default_rows_per_page;
777     }
778    
779    
780     /**
781     * This function sets the save variables
782     * that the user/child wants to automatically
783     * propogate
784     *
785     * @param array - name=>value pairs of the data
786     * that they want to propogate
787     */
788     function set_save_vars( $vars ) {
789     $this->_save_vars = $vars;
790     }
791    
792     /**
793     * This returns the Maximum # of rows to
794     * display when in expand mode
795     *
796     * @return int
797     */
798     function get_max_rows() {
799     return $this->_max_rows;
800     }
801    
802     /**
803     * This sets the maximum # of rows to
804     * display when in expand mode
805     *
806     * @param int - new # of maximum rows
807     * to display when in 'expand' mode
808     *
809     */
810     function set_max_rows( $max ) {
811     $this->_max_rows = $max;
812     }
813    
814    
815    
816     /**
817     * This function is used to set up any
818     * data that needs to be munged before the
819     * data is fetched from the DataListSource
820     *
821     */
822     function data_prefetch() {
823     $this->_check_datasource("data_prefetch");
824    
825     if ($this->showall()) {
826     $this->set_default_num_rows( $this->get_max_rows() );
827     $this->set_numrows( $this->get_max_rows() );
828     }
829    
830     $limit = $this->numrows();
831     $this->_datasource->query($this->offset(), $limit,
832     $this->orderby(), $this->reverseorder(),
833     $this->search_field(), $this->search_value(),
834     $this->simple_search_modifier_value(),
835     $this->search_type() );
836     }
837    
838    
839    
840     /*
841     * Takes an array of (name, sortable, db_field, alignment,
842     * size), with one element corresponding to each column.
843     */
844     function setup_columns() {
845     $temp_headers = array();
846    
847     foreach( $this->_columns as $col_name => $data ) {
848     if ($data["sortorder"] == "reverse") {
849     $data["reverseorder"] = "true";
850     }
851     else {
852     $data["reverseorder"] = "false";
853     }
854     $temp_headers[$col_name] = $data;
855     }
856     $this->_columns = $temp_headers;
857     }
858    
859    
860    
861     /**
862     * general DataListSource object checker.
863     *
864     */
865     function _check_datasource($function_name) {
866     if (!is_object($this->_datasource)) {
867     user_error("DataList::".$function_name."() - DataListSource object is not set");
868     exit;
869     }
870     }
871    
872    
873     /*********************************************/
874     /* REQUEST VARIABLE SECTION */
875     /*********************************************/
876    
877     /**
878     * This function returns the current value
879     * of the offset variable. This is an offset
880     * into the query return data set.
881     *
882     * @return int - the current value.
883     */
884     function offset() {
885     if (!$_REQUEST[$this->_offsetVar]) {
886     $this->set_offset(0);
887     }
888     return (int)$_REQUEST[$this->_offsetVar];
889     }
890    
891     /**
892     * This function is used to set/change
893     * the offset for this list.
894     *
895     * @param int - the new offset.
896     */
897     function set_offset($new_offset) {
898     $_REQUEST[$this->_offsetVar] = $new_offset;
899     }
900    
901     /**
902     * This function returns the value of the
903     * current orderby variable.
904     *
905     * @return string.
906     */
907     function orderby() {
908     if (!$_REQUEST[$this->_orderbyVar]) {
909     $_REQUEST[$this->_orderbyVar] = $this->_default_orderby;
910     }
911    
912     return $_REQUEST[$this->_orderbyVar];
913     }
914    
915     /**
916     * This builds a query string var for the
917     * orderby value.
918     *
919     * @return string - "orderby=(thevalue)"
920     */
921     function build_orderby_querystring() {
922     $str = $this->_orderbyVar."=".$this->orderby();
923     return $str;
924     }
925    
926     /**
927     * This function returns the current value of
928     * the reverse order member variable.
929     *
930     * @return string.
931     */
932     function reverseorder() {
933     if (!$_REQUEST[$this->_reverseorderVar]) {
934     $this->set_reverseorder($this->_default_reverseorder);
935     }
936     return $_REQUEST[$this->_reverseorderVar];
937     }
938    
939     /**
940     * This function sets the reverse order flag
941     * to a new value.
942     *
943     * @param string - the new value.
944     */
945     function set_reverseorder($new_value) {
946     $_REQUEST[$this->_reverseorderVar] = $new_value;
947     }
948    
949     /**
950     * This builds a query string var for the
951     * reverseorder value.
952     *
953     * @return string - "orderby=(thevalue)"
954     */
955     function build_reverseorder_querystring() {
956     $str = $this->_reverseorderVar."=".$this->reverseorder();
957     return $str;
958     }
959    
960     /**
961     * This function returns the number of rows
962     * that the query found.
963     *
964     * @return int - the number of rows
965     */
966     function numrows() {
967     if (!$_REQUEST[$this->_numrowsVar]) {
968     $this->set_numrows($this->_default_rows_per_page);
969     }
970     return (int) $_REQUEST[$this->_numrowsVar];
971     }
972    
973     /**
974     * This function sets the # of rows to display
975     * per page.
976     *
977     * @param int - the # of rows
978     */
979     function set_numrows($new_numrows) {
980     $_REQUEST[$this->_numrowsVar] = $new_numrows;
981     }
982    
983     /**
984     * returns the current value of
985     * the search field name
986     *
987     * @return string
988     */
989     function search_field() {
990     if (!$_REQUEST[$this->_search_fieldVar]) {
991     $_REQUEST[$this->_search_fieldVar] = '';
992     }
993     return $_REQUEST[$this->_search_fieldVar];
994     }
995    
996     /**
997     * This builds a query string var for the
998     * searchfield value.
999     *
1000     * @return string - "orderby=(thevalue)"
1001     */
1002     function build_searchfield_querystring() {
1003     $str = $this->_search_fieldVar."=".$this->searchfield();
1004     return $str;
1005     }
1006    
1007     /**
1008     * returns the current value of
1009     * te search field value.
1010     *
1011     * @return string
1012     */
1013     function search_value() {
1014     if (!$_REQUEST[$this->_search_valueVar]) {
1015     $_REQUEST[$this->_search_valueVar] = '';
1016     }
1017     return $_REQUEST[$this->_search_valueVar];
1018     }
1019    
1020     /**
1021     * This builds a query string var for the
1022     * searchfield value.
1023     *
1024     * @return string - "orderby=(thevalue)"
1025     */
1026     function build_searchvalue_querystring() {
1027     $str = $this->_search_valueVar."=".$this->search_value();
1028     return $str;
1029     }
1030    
1031     /**
1032     * returns the current value of the
1033     * simple search modifier
1034     *
1035     * @return string
1036     */
1037     function simple_search_modifier_value() {
1038     if (!$_REQUEST[$this->_simple_search_modifierVar]) {
1039     $_REQUEST[$this->_simple_search_modifierVar] = '';
1040     }
1041     return $_REQUEST[$this->_simple_search_modifierVar];
1042     }
1043    
1044    
1045     /**
1046     * returns the type of search being used
1047     *
1048     * @return string
1049     */
1050     function search_type() {
1051     if (!$_REQUEST[$this->_search_typeVar]) {
1052     $_REQUEST[$this->_search_typeVar] = "simple";
1053     }
1054     return $_REQUEST[$this->_search_typeVar];
1055     }
1056    
1057     /**
1058     * This function sets the search type
1059     *
1060     * @param string - the search type
1061     * "simple" or "advanced"
1062     */
1063     function set_search_type($type) {
1064     $_REQUEST[$this->_search_typeVar] = $type;
1065     }
1066    
1067    
1068     /**
1069     * returns the current value of the showall
1070     * flag. This tells us if they want the entire
1071     * list of data back from the DB.
1072     *
1073     * @return string - the current value
1074     */
1075     function showall() {
1076     if (!$_REQUEST[$this->_showallVar]) {
1077     $_REQUEST[$this->_showallVar] = 0;
1078     }
1079     return (int) $_REQUEST[$this->_showallVar];
1080     }
1081    
1082     /**
1083     * This is the basic function for letting us
1084     * do a mapping between the column name in
1085     * the header, to the value found in the DB.
1086     *
1087     * NOTE: this function is meant to be overridden
1088     * so that you can push whatever you want.
1089     *
1090     * @param array - $row_data - the entire data for the row
1091     * @param string - $col_name - the name of the column header
1092     * for this row to render.
1093     * @return mixed - either a HTMLTag object, or raw text.
1094     */
1095     function build_column_item($row_data, $col_name) {
1096     $key = $this->_columns[$col_name]["data_name"];
1097    
1098     if ($row_data[$key] == '') {
1099     return "&nbsp;";
1100     } else {
1101     return $this->_filter_column_string($row_data[$key]);
1102     }
1103     }
1104    
1105    
1106     /**
1107     * This does some magic filtering on the data
1108     * that we display in a column. This helps
1109     * to prevent nast data that may have html
1110     * tags in it.
1111     *
1112     * @param string - the column data u want to filter
1113     * @return string the cleaned/filtered data
1114     */
1115     function _filter_column_string($data) {
1116     return htmlspecialchars(trim($data));
1117     }
1118    
1119    
1120     /*******************************************/
1121     /* FORM VARIABLES SECTION */
1122     /*******************************************/
1123    
1124     /**
1125     * This function is used to set the
1126     * form name
1127     *
1128     * @param string
1129     */
1130     function set_form_name($name) {
1131     $this->_form_attributes["name"] = $name;
1132     }
1133    
1134     /**
1135     * This function is used to get
1136     * the form name
1137     *
1138     * @return string
1139     */
1140     function get_form_name() {
1141     return $this->_form_attributes["name"];
1142     }
1143    
1144     /**
1145     * This function is used to set the
1146     * form target
1147     *
1148     * @param string
1149     */
1150     function set_form_target($target) {
1151     $this->_form_attributes["target"] = $target;
1152     }
1153    
1154     /**
1155     * This function is used to get
1156     * the form target
1157     *
1158     * @return string
1159     */
1160     function get_form_target() {
1161     return $this->_form_attributes["target"];
1162     }
1163    
1164     /**
1165     * This function is used to set the
1166     * form method
1167     *
1168     * @param string (POST or GET)
1169     */
1170     function set_form_method($method) {
1171     if ($method != "GET" && $method != "POST") {
1172     user_error("DataList::set_form_method() - INVALID Form method ".$method);
1173     } else {
1174     $this->_form_attributes["method"] = $method;
1175     }
1176     }
1177    
1178     /**
1179     * This function is used to get
1180     * the form method
1181     *
1182     * @return string (POST or GET)
1183     */
1184     function get_form_method() {
1185     return $this->_form_attributes["method"];
1186     }
1187    
1188     /**
1189     * Sets the form action
1190     *
1191     * @param string
1192     */
1193     function set_form_action($action) {
1194     $this->_form_attributes["action"] = $action;
1195     }
1196    
1197     /**
1198     * This function is used to get
1199     * the form action
1200     *
1201     * @return string (POST or GET)
1202     */
1203     function get_form_action() {
1204     return $this->_form_attributes["action"];
1205     }
1206    
1207     /**
1208     * Sets whether to the output into a form
1209     *
1210     * @param bool
1211     */
1212     function set_form_render($flag) {
1213     $this->_form_render_flag = $flag;
1214     }
1215    
1216     /**
1217     * Return the state of the form render
1218     *
1219     * @return bool
1220     */
1221     function get_form_render() {
1222     return $this->_form_render_flag;
1223     }
1224    
1225     /**
1226     * This function is used to set the
1227     * message displayed when no data is found
1228     *
1229     * @param string
1230     */
1231     function set_not_found_message($mesg) {
1232     $this->_check_datasource("set_not_found_message");
1233     $this->_datasource->set_not_found_message($mesg);
1234     }
1235    
1236    
1237     /**
1238     * This function is used to set the value
1239     * of the _show_results_flag
1240     *
1241     * @param boolean - TRUE to show the results
1242     */
1243     function set_show_results( $flag=TRUE ) {
1244     $this->_show_results_flag = $flag;
1245     }
1246    
1247    
1248     /**
1249     * This function is used to let render() know
1250     * that we should show the results or not.
1251     *
1252     * @return boolean
1253     */
1254     function _show_results() {
1255     return $this->_show_results_flag;
1256     }
1257    
1258     /**
1259     * this method builds some hidden
1260     * form fields to automatically
1261     * be placed inside the form.
1262     *
1263     * @return ContainerWidget object.
1264     */
1265     function _build_save_vars() {
1266     $container = container();
1267     foreach($this->_save_vars as $name => $value) {
1268     $container->add(form_hidden($name, $value));
1269     }
1270     return $container;
1271     }
1272    
1273     /**
1274     * This function sets the save variables
1275     * that the user/child wants to automatically
1276     * propogate
1277     *
1278     * @param array - name=>value pairs of the data
1279     * that they want to propogate
1280     */
1281     function set_save_vars($vars) {
1282     $this->_save_vars = $vars;
1283     }
1284    
1285     /**
1286     * This builds the base url used
1287     * by the column headers as well
1288     * as the page tool links.
1289     *
1290     * it basically builds:
1291     * $_SELF?$_GET
1292     *
1293     * @return string
1294     */
1295     function build_base_url() {
1296    
1297     $url = $_SERVER["PHP_SELF"]."?";
1298    
1299     if ($this->get_form_method() == "POST") {
1300     return $url;
1301     }
1302    
1303     $vars = array_merge($_POST, $_GET);
1304     //request method independant access to
1305     //browser variables
1306     if (count($vars)) {
1307     //walk through all of the get vars
1308     //and add them to the url to save them.
1309     foreach($vars as $name => $value) {
1310    
1311     if ($name != $this->_offsetVar &&
1312     $name != $this->_orderbyVar &&
1313     $name != $this->_reverseorderVar &&
1314     $name != $this->_search_valueVar
1315     ) {
1316     if (is_array($value)) {
1317     $url .= $name."[]=".implode("&".$name."[]=",$value)."&";
1318     } else {
1319     $url .= $name."=".urlencode(stripslashes($value))."&";
1320     }
1321     }
1322     }
1323     }
1324    
1325     return $url;
1326     }
1327    
1328     /**
1329     * This function builds the 'tool' images that
1330     * allow you to walk through the data list itself.
1331     * It provides image links for
1332     * first - go to the first page in the data list
1333     * prev - go to the previous page in the data list
1334     * next - go to the next page in the data list
1335     * last - go to the last page in the data list
1336     * all - show the rest of the list from the current offset
1337     *
1338     * @param string - which tool image to build
1339     * @return Object
1340     */
1341     function build_tool_link( $which ) {
1342     $num_pages = $this->get_num_pages();
1343     $cur_page = $this->get_current_page();
1344     $last_page = $this->get_last_page();
1345    
1346     $image_path = $this->get_image_path();
1347     switch ($which) {
1348    
1349     case "first":
1350     $rows_string = "First ".$this->get_default_num_rows()." Rows";
1351     if ($this->offset() <= 0) {
1352     $obj = html_img($image_path."/first_group_button_inactive.gif",
1353     '','',0,$rows_string, NULL, $rows_string);
1354     } else {
1355     $url = $this->_build_tool_url(0);
1356     $obj = html_img_href($url, $image_path."/first_group_button.gif",
1357     '','',0, $rows_string, NULL, NULL, $rows_string);
1358     }
1359     break;
1360    
1361     case "prev":
1362     $rows_string = "Previous ".$this->get_default_num_rows()." Rows";
1363     if ($this->offset() <= 0) {
1364     $obj = html_img($image_path."/prev_group_button_inactive.gif",
1365     '','',0,$rows_string, NULL, $rows_string);
1366     } else {
1367     $offset = $this->offset() - $this->numrows();
1368     if ($offset < 0) {
1369     $offset = 0;
1370     }
1371     $url = $this->_build_tool_url($offset);
1372     $obj = html_img_href($url, $image_path."/prev_group_button.gif",
1373     '','',0, $rows_string, NULL, NULL, $rows_string);
1374     }
1375     break;
1376    
1377     case "next":
1378     $rows_string = "Next ".$this->get_default_num_rows()." Rows";
1379     if (($num_pages == 1) || ($cur_page == $last_page)) {
1380     $obj = html_img($image_path."/next_group_button_inactive.gif",
1381     '','',0, $rows_string, NULL, $rows_string);
1382     } else {
1383     $offset = $this->offset() + $this->numrows();
1384     $url = $this->_build_tool_url($offset);
1385     $obj = html_img_href($url, $image_path."/next_group_button.gif",
1386     '','',0, $rows_string, NULL, NULL, $rows_string);
1387     }
1388     break;
1389    
1390     case "last":
1391     $rows_string = "Last ".$this->get_default_num_rows()." Rows";
1392     if (($num_pages == 1) || ($cur_page == $last_page)) {
1393     $obj = html_img($image_path."/last_group_button_inactive.gif",
1394     '','',0, $rows_string, NULL, $rows_string);
1395     } else {
1396     $offset = (int)(($num_pages - 1) * $this->numrows());
1397     $url = $this->_build_tool_url($offset);
1398     $obj = html_img_href($url, $image_path."/last_group_button.gif",
1399     '','',0, $rows_string, NULL, NULL, $rows_string);
1400     }
1401     break;
1402    
1403     case "all":
1404     $offset = $this->offset();
1405     if ($this->showall()) {
1406     $url = $this->_build_tool_url($offset, TRUE, 0);
1407     $obj = html_img_href($url, $image_path."/close_group_button.gif",
1408     '','',0,"Collapse Rows", NULL, NULL, "Collapse Rows");
1409     } else {
1410     if (($num_pages == 1)) {
1411     $obj = html_img($image_path."/expand_group_button_inactive.gif",
1412     '','',0, "Expand Rows", NULL, "Expand Rows");
1413     } else {
1414     $url = $this->_build_tool_url($offset, TRUE, 1);
1415     $obj = html_img_href($url, $image_path."/expand_group_button.gif",
1416     '','',0,"Expand Rows", NULL, NULL, "Expand Rows");
1417     }
1418     }
1419     //so we don't save it into the mozilla navigation bar links
1420     unset($url);
1421     break;
1422     }
1423    
1424     if ($url) {
1425     $this->_save_mozilla_nav_link($which, $url);
1426     }
1427    
1428     return $obj;
1429     }
1430    
1431     /**
1432     * This function is used to build the url
1433     * for a tool link.
1434     * (first, prev, next, last, all)
1435     *
1436     * @param int - the offset for the link
1437     * @param boolean - add the showall value to the url
1438     * @param int - the showall value to use if the flag is on
1439     *
1440     * @return string
1441     */
1442     function _build_tool_url($offset, $showall_flag=FALSE, $showall_value=0) {
1443     if ($this->get_form_method() == "POST") {
1444     $form_name = $this->get_form_name();
1445     $url = "javascript: document.".$form_name;
1446     $url .= ".".$this->_offsetVar.".value='".$offset."';";
1447    
1448     //add the showall variable to the post
1449     if ($showall_flag) {
1450     $form_field = $this->_showallVar;
1451     $url .= "document.".$form_name.".";
1452     $url .= $form_field.".value='".$showall_value."';";
1453     }
1454    
1455     $url .= "document.".$form_name.".submit();";
1456     } else {
1457     $url = $this->build_base_url();
1458     $url .= $this->build_state_vars_query_string($offset, $showall_flag,
1459     $showall_value);
1460     }
1461     return $url;
1462     }
1463    
1464     /**
1465     * this function is used to build a sub query string
1466     * of all of the query string vars to save the
1467     * state of the DBItemList. This is used for pages
1468     * that want to come back to the list at the same state
1469     *
1470     * @return string - name=value& pairs
1471     */
1472     function build_state_vars_query_string($offset, $showall_flag=FALSE,
1473     $showall_value=0) {
1474     $str = "";
1475    
1476     $str .= $this->_offsetVar."=".urlencode($offset);
1477     $str .= "&".$this->_orderbyVar."=".urlencode($this->orderby());
1478     $str .= "&".$this->_reverseorderVar."=".urlencode($this->reverseorder());
1479     $str .= "&".$this->_search_fieldVar."=".urlencode($this->search_field());
1480     $str .= "&".$this->_search_valueVar."=".urlencode($this->search_value());
1481     $str .= "&".$this->_simple_search_modifierVar."=".urlencode($this->simple_search_modifier_value());
1482     $str .= "&".$this->_search_typeVar."=".urlencode($this->search_type());
1483     if ($showall_flag) {
1484     $str .= "&".$this->_showallVar."=".urlencode($showall_value);
1485     }
1486    
1487     return $str;
1488     }
1489    
1490     /**
1491     * This function stores the url for each of the tool
1492     * urls, so we can push these out for mozilla.
1493     * Mozilla has a nice navigation bar feature that
1494     * lets you program first, prev, next, last links
1495     *
1496     * @param string - which tool link
1497     * @param string - the url for that link
1498     */
1499     function _save_mozilla_nav_link($which, $url) {
1500     $this->_mozilla_nav_links[$which] = $url;
1501     }
1502    
1503    
1504     /**
1505     * This function returns the path to the
1506     * images used in this class
1507     *
1508     * @return string
1509     */
1510     function get_image_path() {
1511     return $this->_image_path;
1512     }
1513    
1514     /**
1515     * This function returns the path to the
1516     * images used in this class
1517     *
1518     * @return string
1519     */
1520     function set_image_path($path) {
1521     return $this->_image_path = $path;
1522     }
1523    
1524     /**
1525     * This function returns the current
1526     * page that the item list is on.
1527     *
1528     * @return int
1529     */
1530     function get_current_page() {
1531     return((int) ($this->offset() / $this->numrows())) + 1;
1532     }
1533    
1534     /**
1535     * This function returns the #
1536     * of pages that are available
1537     * for this list of items.
1538     *
1539     * @return int
1540     */
1541     function get_num_pages() {
1542     $this->_check_datasource("get_num_pages");
1543     $total_rows = $this->_datasource->get_total_rows();
1544    
1545     $cnt = (int)($total_rows / $this->numrows());
1546     if ((($total_rows % $this->numrows()) != 0) || ($total_rows == 0)) {
1547     $cnt++;
1548     }
1549     return $cnt;
1550     }
1551    
1552     /**
1553     * This calculates the last page #
1554     * for this list of items
1555     *
1556     * @return int
1557     */
1558     function get_last_page() {
1559     return $this->get_num_pages();
1560     }
1561    
1562     /**
1563     * This function builds the string
1564     * that describes the current page
1565     * out of n pages the list is showing
1566     *
1567     * @return string (ie. 1 to 10 of 25)
1568     */
1569     function get_page_info() {
1570     $cur_page = $this->get_current_page();
1571     $low_range = $this->offset() + 1;
1572     $num_pages = $this->get_num_pages();
1573     $num_rows_per_page = $this->numrows();
1574    
1575     $this->_check_datasource("get_page_info");
1576     $total_rows = $this->_datasource->get_total_rows();
1577    
1578     $high_range = $low_range + ($num_rows_per_page - 1);
1579     if ($high_range > $total_rows) {
1580     $high_range = $total_rows;
1581     }
1582    
1583     if ($total_rows == 0) {
1584     $str = "0 of 0";
1585     } else {
1586     $str = $low_range . " to ". $high_range;
1587     $str .= " of ".$total_rows;
1588     }
1589    
1590     return $str;
1591     }
1592    
1593    
1594    
1595     /**
1596     * This builds a url for a particular
1597     * column header.
1598     *
1599     * @param string - $col_name
1600     * @return Atag object;
1601     */
1602     function build_column_url($col_name) {
1603     $orderby = $this->orderby();
1604     $reverseorder = $this->reverseorder();
1605     $search_value = $this->search_value();
1606    
1607     $order_value = $this->_columns[$col_name]["data_name"];
1608     $reverse_value = "false";
1609     if (!$reverseorder) {
1610     $reverseorder = 'false';
1611     }
1612    
1613     if ($orderby == $order_value && $reverseorder === 'false') {
1614     $reverse_value = "true";
1615     }
1616    
1617     if ($this->form_method == "POST") {
1618     //we have to construct this url
1619     //specially.
1620     $form_name = $this->get_form_name();
1621    
1622     $url = "javascript: ";
1623     //set the offset correctly
1624     $url .= "document.".$form_name.".".$this->_offsetVar.".value='".$this->offset()."';";
1625     //set the orderby correctly.
1626     $url .= "document.".$form_name.".".$this->_orderbyVar.".value='".$order_value."';";
1627     //set the reverseorder
1628     $url .= "document.".$form_name.".".$this->_reverseorderVar.".value='".$reverse_value."';";
1629    
1630     $url .= "document.".$form_name.".submit();";
1631     } else {
1632     //handle the normal get.
1633     $url = $this->build_base_url();
1634     //Now add the orderby, reverseorder and offset vars
1635     $url .= $this->_offsetVar ."=0&";
1636     //set the orderbyvar
1637     $url .= $this->_orderbyVar ."=".$order_value."&";
1638     //set the reverseorder
1639     $url .= $this->_reverseorderVar ."=".$reverse_value."&";
1640     //set the search value
1641     $url .= $this->_search_valueVar ."=".$search_value;
1642     }
1643    
1644     return $url;
1645     }
1646    
1647     /**
1648     * This is the basic function for letting us
1649     * do a mapping between the column name in
1650     * the header, to the value found in the DB.
1651     *
1652     * NOTE: this function is meant to be overridden
1653     * so that you can push whatever you want.
1654     *
1655     * @param array - $row_data - the entire data for the row
1656     * @param string - $col_name - the name of the column header
1657     * for this row to render.
1658     * @return mixed - either a HTMLTag object, or raw text.
1659     */
1660     function build_column_item($row_data, $col_name) {
1661     $key = $this->_columns[$col_name]["data_name"];
1662    
1663     if ($row_data[$key] == '') {
1664     return "&nbsp;";
1665     } else {
1666     return $this->filter_column_string($row_data[$key]);
1667     }
1668     }
1669    
1670    
1671     /**
1672     * This does some magic filtering on the data
1673     * that we display in a column. This helps
1674     * to prevent nast data that may have html
1675     * tags in it.
1676     *
1677     * @param string - the column data u want to filter
1678     * @return string the cleaned/filtered data
1679     */
1680     function filter_column_string($data) {
1681     return htmlspecialchars(trim($data));
1682     }
1683    
1684    
1685    
1686     /**
1687     * This function is used to make sure that the string we are
1688     * placing in a cell has been "cleaned"
1689     *
1690     * @param mixed - the cell object. It can be a string.
1691     * @param string - the name of the column this object/string
1692     * will live
1693     *
1694     * @return mixed - the cleaned string or object
1695     */
1696     function _clean_string($obj, $col_name){
1697     if (is_string($obj)) {
1698     if ($this->_columns[$col_name]["maxtextlength"]) {
1699     //looks like we need to make sure we
1700     //truncate the string to a max length
1701     if (strlen($obj) > $this->_columns[$col_name]["maxtextlength"]) {
1702     //we need to truncate it.
1703     $obj = substr($obj, 0, $this->_columns[$col_name]["maxtextlength"]);
1704     $obj .= "...";
1705     }
1706     }
1707     }
1708     return $obj;
1709     }
1710    
1711     /********************************/
1712     /* SEARCH RELATED */
1713     /********************************/
1714    
1715     /**
1716     * This method gets the array of
1717     * searchable header fields (columns)
1718     *
1719     * @return array
1720     */
1721     function _get_searchable_fields() {
1722     $fields = array();
1723     foreach($this->_columns as $name => $header) {
1724     if ($header["searchable"] == TRUE) {
1725     $fields[$name] = $header["data_name"];
1726     }
1727     }
1728     return $fields;
1729     }
1730    
1731     /**
1732     * This builds the simple search modifier
1733     * select box.
1734     *
1735     * @return INPUTtag object.
1736     */
1737     function _build_simple_search_modifier() {
1738    
1739     $options = array();
1740    
1741     $modifier = $this->get_simple_search_modifier();
1742    
1743     if ($modifier & SEARCH_BEGINS_WITH) {
1744     $options["beginning with"] = "BEGINS";
1745     }
1746     if ($modifier & SEARCH_CONTAINS) {
1747     $options["containing"] = "CONTAINS";
1748     }
1749     if ($modifier & SEARCH_EXACT) {
1750     $options["matching"] = "EXACT";
1751     }
1752     if ($modifier & SEARCH_ENDS_WITH) {
1753     $options["ending with"] = "ENDS";
1754     }
1755    
1756     $selected = $this->simple_search_modifier_value();
1757     //make the default Begins with
1758     if (!$selected) {
1759     $selected = "BEGINS";
1760     }
1761    
1762     return form_select($this->_simple_search_modifierVar, $options, $selected);
1763     }
1764    
1765     /**
1766     * This function is used to make safe
1767     * any query string value that is used
1768     *
1769     * @param string
1770     * @return string
1771     */
1772     function search_value_filter( $value ) {
1773     return stripslashes( trim($value) );
1774     }
1775     }
1776     ?>

MailToCvsAdmin">MailToCvsAdmin
ViewVC Help
Powered by ViewVC 1.1.26 RSS 2.0 feed