We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
    • 28559
    • 23 Posts
    Thanks That worked !

    SteveO
      • 24731 ☆ A M B ☆
      • 211 Posts
      Hello, I made a very simple modification to this... basically just added an extra parameter so I could control the recurring events postings more:

      $calendar_xml_address = "http://www.google.com/calendar/feeds/".$email."/public/basic?start-min=".date("Y-m-d").'&start-max='.$startMax.'&singleevents=' . $singleEvents;


      It works as I wanted now when I make the call, except that with this approach I loose the start and end time for the recurring events.

      I did a small debug on it and printed the $useInfo variable where the start and end time is taken from.

      I discovered that the first array I get, the one with recurring events, comes out empty, so that explains why there is no time details in it (I guess).

      Any ideas how to sort this out?

      This is my XML:

      http://www.google.com/calendar/feeds/q5t0dc56cb5hgkoshdnv0hrqfc%40group.calendar.google.com/public/basic?start-min=2007-11-12&start-max=2007-11-30&singleevents=false

      This is my snippet code:

      <?php
      // Resource Name: GoogleEvents
      // Author: Jesse Rochman
      // Forum Alias: jesster444
      // Version: 1.3.4
      // Compatible with .951
      // Released: 1/9/2007
      // Modified: 3/1/2007 by KyleJ
      // Modified: 3/2/2007 by Jesse
      // Inspired by James Cridlands Google Calendar Code
      //Calendar Class is from Giorgos Tsiledakis Active Calendar
      
      $basePath = $modx->config['base_path'];
      $siteURL = $modx->config['site_url'];
      
      // Your Gmail address
      $email = isset($email)? $email : "[email protected]";
      
      //Set month and day names.  You can translate here.
      $dayNames1 = array('S','M','T','W','T','F','S');
      $dayNames3 = array('Sun','Mon','Tue','Wed','Thur','Fri','Sat');
      $dayNamesFull = array('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');
      $monthNames = array('January','February','March','April','May','June','July','August','September','October','November','December');
      
      if (!isset ($dayNameSize)) {
      	$dayNameSize = $dayNames3;
      	} elseif (strtolower($dayNameSize) == 'full') {
      	$dayNameSize = $dayNamesFull;
      	} elseif ($dayNameSize == 1) {
      	$dayNameSize = $dayNames1;
      	} else {
      	$dayNameSize = $dayNames3;
      	}
      
      //This will determine whether to output the calendar, the list, or both.
      $outputType = isset($outputType)? $outputType : "both";
      
      // Format for date and time
      $dateFormat = isset($dateFormat)? $dateFormat : "l, F, jS, Y"; // Thursday, 10 March - see http://www.php.net/date for details
      $timeFormat = isset($timeFormat)? $timeFormat : "g:ia";
      
      //Order of Output.  True for Reverse Chronilogical and False for Chronilogical
      $order = isset($order)? $order : 0;
      
      //Show Events this many days from now
      $singleEvents = isset($singleEvents)? $singleEvents : 'true';
      
      //Show Events For This Many Days Out
      $lastEvent = isset($lastEvent)? $lastEvent : 30;
      
      //Show only this many events
      $maxEvents = isset($maxEvents)? $maxEvents : 30;
      
      //Skip this many events.  This way you can make two snippet calls and jump around
      $skipEvents = isset($skipEvents)? $skipEvents : 0;
      
      // Cache location for your XML file
      $cache_location= $basePath.'assets/snippets/GoogleEvents/cache';
      
      // Here is where your copy of Simplepie lives
      include_once ($basePath.'assets/snippets/GoogleEvents/php/simplepie.inc');
      
      //Sets a big or small calendar
      $calendarType = isset($calendarType)? $calendarType : '';
      
      //Determine the first day of the week
      $firstDay = isset($firstDay)? $firstDay : 0;
      
      // Change this to 'true' to see some debug code
      $debug = isset($debug)? $debug : 0;
      
      //Config ends
      
      if ($outputType == 'calendar' || $outputType == 'both') {
      	include_once('assets/snippets/GoogleEvents/source/activecalendar.php');
      	//For the calendar script
      	//Variables for Calendar
      	$myurl='?id='.$modx->documentIdentifier; // the links url is this page
      	$yearID=false; // init false to display current year
      	$monthID=false; // init false to display current month
      	$dayID=false; // init false to display current day
      	extract($_GET); // get the new values (if any) of $yearID,$monthID,$dayID
      	$arrowBack="<img src=\"assets/snippets/GoogleEvents/data/img/back.png\" alt=\"<<\" id=\"gnavbak\" />"; // use png arrow back
      	$arrowForw="<img src=\"assets/snippets/GoogleEvents/data/img/forward.png\" alt=\">>\" id=\"gnavfor\" />"; // use png arrow forward
      	$cal = new activeCalendar($yearID,$monthID,$dayID);
      }
      
      if ($outputType == 'list' || $outputType == 'both') {	
      	//Grab the templates.  If one isn't passed to the program, then setup a default template.
      	$defaultTemplate = '<div><h2><a href="[+linkURL+]">[+title+]</a></h2><p>[+startDate+] at [+startTime+]<br />Description: [+content+]</p></div>';
      	$numberOfEvents = 0;
      	$tpl = $modx->getChunk($tpl);
      	$tplOddRow = $modx->getChunk($tplOddRow);
      	$tplEvenRow = $modx->getChunk($tplEvenRow);
      	$tplFirstRow = $modx->getChunk($tplFirstRow);
      	$tplLastRow = $modx->getChunk($tplLastRow);
      
      	if ($tpl) {
      	  $defaultTemplate = $tpl;
      	}
      	if (!$tplOddRow) {
      	  $tplOddRow = $defaultTemplate;
      	}
      	if (!$tplEvenRow) {
      	  $tplEvenRow = $defaultTemplate;
      	}
      	if (!$tplFirstRow) {
      	  $tplFirstRow = $defaultTemplate;
      	}
      	if (!$tplLastRow) {
      	  $tplLastRow = $defaultTemplate;
      	}
      }
      
      $startMax = date('Y-m-d',mktime(0, 0, 0, date("m")  , date("d")+$lastEvent, date("Y")));
      
      if ($debug) {echo "<p>Debug mode is on.</p>";}
      
      $calendar_xml_address = "http://www.google.com/calendar/feeds/".$email."/public/basic?start-min=".date("Y-m-d").'&start-max='.$startMax.'&singleevents=' . $singleEvents;
      
      if ($debug) {echo "<p>Getting <a href='$calendar_xml_address'>XML feed</a></p>";}
      
      $feed = new SimplePie();
      $feed->feed_url($calendar_xml_address);
      $feed->cache_location($cache_location);
      //$feed->enable_caching(FALSE);
      //$feed->enable_xmldump(true);
      $feed->init();
      $feed->handle_content_type();
      
      if($feed->get_items()) {
      	foreach($feed->get_items() as $item) {	
      		//echo '<pre>' . print_r($item,1) . '</pre>';
      	    $description = $item->get_description();
      		if ($description) {
      		    if ($debug) { echo "<p><b>Description to be parsed:</b> <br />".$description."</p>"; }
      			
      			//Remove some of the html entity oddities that Google produces like &amp; which displays "&"
      			$description = str_replace ("&amp;","&",$description);
      			$description = str_replace ("&#39;","&#39;",$description);
      			$description = str_replace ("&quot;",""",$description);
      			
      			// Extract the 'when' information from the item summary, and ignore everything else
      			$from = trim(textBetween("When:","<",$description));
      			if (strlen($from) > 16) {
      				$from = trim(textBetween("When:","&",$description));
      			}
      			
      			//Put all event information into an array
      			$from = str_replace(',','',$from);
      			$fromInfo = explode(" ",$from);
      
      			if (count($fromInfo) < 8 ) {
      				$useInfo = array(
      					'startDow' => $fromInfo[0],
      					'startMon' => $fromInfo[1],
      					'startDay' => $fromInfo[2],
      					'startYear' => $fromInfo[3],
      					'startTime' => $fromInfo[4],
      					'endDow' => $fromInfo[0],
      					'endMon' => $fromInfo[1],
      					'endDay' => $fromInfo[2],
      					'endYear' => $fromInfo[3],
      					'endTime' => $fromInfo[6],
      				);
      			} else {
      				$useInfo = array(
      					'startDow' => $fromInfo[0],
      					'startMon' => $fromInfo[1],
      					'startDay' => $fromInfo[2],
      					'startYear' => $fromInfo[3],
      					'startTime' => $fromInfo[4],
      					'endDow' => $fromInfo[6],
      					'endMon' => $fromInfo[7],
      					'endDay' => $fromInfo[8],
      					'endYear' => $fromInfo[9],
      					'endTime' => $fromInfo[10],
      				);
      			}
      			//Add a leading zero to the day if none
      			if (strlen($useInfo['startDay']) < 2) { $useInfo['startDay'] = "0{$useInfo['startDay']}"; }
      			if (strlen($useInfo['endDay']) < 2) { $useInfo['endDay'] = "0{$useInfo['endDay']}"; }
      			//Make start time include mintues and leading 0 if necessary for correct time calculation
      			if (strlen($useInfo['startTime']) == 3) { $useInfo['startTime'] = '0' . substr($useInfo['startTime'],0,1) . ':00' . substr($fromInfo['startTime'],1,2); }
      			if (strlen($useInfo['startTime']) == 4) { $useInfo['startTime'] = substr($useInfo['startTime'],0,2) . ':00' . substr($useInfo['startTime'],2,2); }
      			if (strlen($useInfo['startTime']) == 6) { $useInfo['startTime'] = '0' . $useInfo['startTime']; }
      			//Make end time include mintues and leading 0 if necessary for correct time calculation
      			if (strlen($useInfo['endTime']) == 3) { $useInfo['endTime'] = '0' . substr($useInfo['endTime'],0,1) . ':00' . substr($useInfo['endTime'],1,2); }
      			if (strlen($useInfo['endTime']) == 4) { $useInfo['endTime'] = substr($useInfo['endTime'],0,2) . ':00' . substr($useInfo['endTime'],2,2); }
      			if (strlen($useInfo['endTime']) == 6) { $useInfo['endTime'] = '0' . $useInfo['endTime']; }
      			
      			//Convert start date & time to a unix time stamp & fill other vars
      			$tempStartDate = strtotime("{$useInfo['startDay']} {$useInfo['startMon']} {$useInfo['startYear']} {$useInfo['startTime']}");
      			$startDate = date($dateFormat,$tempStartDate);
      			$startDay = $useInfo['startDay'];
      			$startYear = $useInfo['startYear'];
      			$startTime = $useInfo['startTime'] ? date($timeFormat,$tempStartDate) : '';
      			$startMonthCal = date('n',$tempStartDate);
      			
      			//Convert end date & time to a unix time stamp & fill other vars
      			$tempEndTime = strtotime("{$useInfo['endDay']} {$useInfo['endMon']} {$useInfo['endYear']} {$useInfo['endTime']}");
      			$endDate = date($dateFormat,$tempEndTime);
      			$endTime = $useInfo['endTime'] ? date($timeFormat,$tempEndTime) : '';
      
      			$time = $tempStartDate;	
      			if ($debug) { 
      			echo "<p><b>Atempting to parse the following date:</b><br />$from</p>
      				  <p><b>Parsed Date:</b><br />
      					 Starting Day: $startDate<br />
      					 Starting Time: $startTime<br />
      					 Ending Day: $endDate<br />
      					 Ending Time: $endTime<br />
      					 Time Greater Than 0? $time</p>";
      					 }
      
      			// Extract the 'where' information from the item summary, and ignore everything else
      			$where = textBetween("Where:","<",$description);
      			if (empty ($where)) $where = '';
      			if ($debug) {
      				echo "<p><b>This is what I think the "Where" string should be:</b><br />$where</p>";
      			}
      			
      			// Extract the "Event Description" information from the item summary, and ignore everything else
      			if (strpos($description,"Event Description: ")) {
      				$eventDescription = substr($description,strpos($description,"Event Description: ")+18);
      			} else {
      				$eventDescription = '';
      			}
      
      			// Extract the link for Google Calendar
      			$url=$item->get_links();
      
      			// Stick into a nice array if it has a valid timestamp (not a recurring event).
      			if ($time > 0) {
      				$events[]=array('time'=>$time,'where'=>$where,'title'=>$item->get_title(),'url'=>$url[0],'eventDescription'=>$eventDescription,'startDate'=>$startDate,'startTime'=>$startTime,'endDate'=>$endDate,'endTime'=>$endTime,'startYear'=>$startYear,'startDay'=>$startDay,'startMonthCal'=>$startMonthCal);
      			}
      		}
      	}
      
      	// Sort the bookings into time order
      	if (!$order){
      		// Sort the events into chronilogical order.
      		sort($events);
      		} else {
      		rsort($events);
      		}
      
      	$counterMaxEvents = 0;
      	$counterSkipEvents = 0;
      	foreach($events as $event) {
      		if ($outputType == 'list' || $outputType == 'both') {
      			$numberOfEvents++;
      			if ($counterSkipEvents >= $skipEvents){
      				//Figure out what template to use
      				if($numberOfEvents == 1 || $counterSkipEvents == $skipEvents) {
      					$template = $tplFirstRow;
      					} elseif ($numberOfEvents == count($events) || $counterMaxEvents == ($maxEvents-1)) {
      					$template = $tplLastRow;
      					} elseif (($numberOfEvents & 1) == 0) {
      					$template = $tplEvenRow;
      					} else {
      					$template = $tplOddRow;
      					}
      				$counterSkipEvents++;
      				//Output the data in the appropriate template
      				if ($counterMaxEvents < $maxEvents) {
      					$output = str_replace('[+title+]',$event['title'],$template);
      					$output = str_replace('[+startDate+]',$event['startDate'],$output);
      					$output = str_replace('[+startTime+]',$event['startTime'],$output);
      					$output = str_replace('[+endDate+]',$event['endDate'],$output);
      					$output = str_replace('[+endTime+]',$event['endTime'],$output);
      					$output = str_replace('[+where+]',$event['where'],$output);
      					$output = str_replace('[+content+]',$event['eventDescription'],$output);
      					$output = str_replace('[+eventNumber+]',$numberOfEvents,$output);
      					$output = str_replace('[+linkURL+]',$event['url'],$output);
      					echo $output;
      					$counterMaxEvents++;
      					} 
      			} else { 
      				$counterSkipEvents++;
      				}
      			} 
      			if($outputType == 'calendar' || $outputType == 'both') {
      				$cal->setEvent($event['startYear'],$event['startMonthCal'],$event['startDay'],"event",$event['url']);
      				if($calendarType == 'wide') {
      					$multipleLinesEvent=array($event['title'],$event['startTime'].'-'.$event['endTime']);
      					$cal->setEventContent($event['startYear'],$event['startMonthCal'],$event['startDay'],$multipleLinesEvent,$event['url']);
      				}
      			}
      	}	
      	if($outputType == 'calendar' || $outputType == 'both') {
      			//outPutCalendar($fullMonths, $shortMonths,$mDays, $arrangedEvents);
      			$cal->setDayNames($dayNameSize);
      			$cal->setMonthNames($monthNames);
      			$cal->setFirstWeekDay($firstDay);
      			$cal->enableMonthNav($myurl,$arrowBack,$arrowForw); // enables navigation controls
      			print $cal->showMonth();
      	}
      
      	return '';
      	} else {
      	return "<p>The feed is unavailable.  This could be caused by the server being down, an incorrect feed address, or if you have no events in your feed.  The feed address you tried to load was <a href='$calendar_xml_address'>$calendar_xml_address</a>.</p>";
      	}
      
      function textBetween($s1,$s2,$s){
        $s1 = strtolower($s1);
        $s2 = strtolower($s2);
        $L1 = strlen($s1);
        $scheck = strtolower($s);
      
        if($L1>0){$pos1 = strpos($scheck,$s1);} else {$pos1=0;}
        if($pos1 !== false){
         if($s2 == '') return substr($s,$pos1+$L1);
         $pos2 = strpos(substr($scheck,$pos1+$L1),$s2);
         if($pos2!==false) return substr($s,$pos1+$L1,$pos2);
        }
        return '';
      }
      ?>


      and this is my snippet call:

      [!GoogleEvents? &email=`q5t0dc56cb5hgkoshdnv0hrqfc%40group.calendar.google.com` &outputType=`list` &lastEvent=`30` &maxEvents=`20` &dayNameSize=`3` &calendarType=`small` &firstDay=`1` &tplFirstRow=`GoogleEvents` &tplOddRow=`GoogleEventsOdd` &tplEvenRow=`GoogleEventsEven` &tplLastRow=`GoogleEventsLast` &singleEvents=`false`!]


      Here is my debug output:

      Debug mode is on.

      Getting XML feed

      Description to be parsed:
      Recurring Event
      First start: 2007-10-29 20:30:00 BOT
      Duration: 3600
      Where: Teatro Las Máscaras, Zona Colonial
      Event Status: confirmed
      Event Description: Director: Germana Quintana Cast: Lidia Ariza, Josefina Muñoz, Osiris Ventura Where: Teatro Las Máscaras, Zona Colonial When: 20:30 Mon, Tue, Thu

      Atempting to parse the following date:

      Parsed Date:
      Starting Day: Monday, November, 12th, 2007
      Starting Time:
      Ending Day: Monday, November, 12th, 2007
      Ending Time:
      Time Greater Than 0? 1194840000

      This is what I think the "Where" string should be:
      Teatro Las Máscaras, Zona Colonial

      Description to be parsed:
      Recurring Event
      First start: 2007-10-29 18:30:00 BOT
      Duration: 3600
      Where: Teatro Las Máscaras, Zona Colonial
      Event Status: confirmed
      Event Description: Director: Osiris Ventura Cast: Lidia Ariza, Josefina Muñoz, Osiris Ventura Where: Teatro Las Máscaras, Zona Colonial When: 18:30 weekends

      Atempting to parse the following date:

      Parsed Date:
      Starting Day: Monday, November, 12th, 2007
      Starting Time:
      Ending Day: Monday, November, 12th, 2007
      Ending Time:
      Time Greater Than 0? 1194840000

      This is what I think the "Where" string should be:
      Teatro Las Máscaras, Zona Colonial

      Description to be parsed:
      Recurring Event
      First start: 2007-10-29 18:30:00 BOT
      Duration: 3600
      Where: Sala Ravelo
      Event Status: confirmed
      Event Description: When: 18:30 Fri; 18:00 Sat, Dom Cover: RD$300

      Atempting to parse the following date:

      Parsed Date:
      Starting Day: Monday, November, 12th, 2007
      Starting Time:
      Ending Day: Monday, November, 12th, 2007
      Ending Time:
      Time Greater Than 0? 1194840000

      This is what I think the "Where" string should be:
      Sala Ravelo
        • 14050
        • 788 Posts
        This snippet does not support recurring events unless singleevents is set to "true."

        Single Events does this:

        singleevents=true: recurring events are represented in the same format as single events, with a single entry element per occurrence of the event. Each entry includes a single gd:when element, but does not include the gd:recurrence syntax. It does, however, include a gd:originalEvent element.

        Is there any reason you need recurring events to not be represented the same way single events in the XML feed?
          Jesse R.
          Consider trying something new and extraordinary.
          Illinois Wine

          Have you considered donating to MODx lately?
          Donate now. Every contribution helps.
          • 24731 ☆ A M B ☆
          • 211 Posts
          Quote from: Jesse at Nov 12, 2007, 11:00 PM

          This snippet does not support recurring events unless singleevents is set to "true."

          Single Events does this:

          singleevents=true: recurring events are represented in the same format as single events, with a single entry element per occurrence of the event. Each entry includes a single gd:when element, but does not include the gd:recurrence syntax. It does, however, include a gd:originalEvent element.

          Is there any reason you need recurring events to not be represented the same way single events in the XML feed?

          Yes, well I had problems with recurring events because the Calendar I am using relies mostly on those. Think about theater plays that span from x time to x time every day at a given time. I display events for the next 30 days, so when using single events I receive the events repeated over and over for all days. I would rather instead have just one event presented for that span of time the way the recurring events are presented so that the display is not so cluttered.

          Another approach I wanted to try was including a parameter for the starting date and then just make several calls with different date coverage so I can present today’s., tomorrow and the day after tomorrow and so on. That would also give me a cleaner output. But the best output I have been able to acquired so far is through the variable explained before, except I loose the start time and end time.

          Do you have any suggestions?

          Jose
            • 14050
            • 788 Posts
            I think doing the start date parameter would be the only solution I can think of at the moment.

            $calendar_xml_address = "http://www.google.com/calendar/feeds/".$email."/public/basic?start-min=".date("Y-m-d").'&start-max='.$startMax.'&singleevents=' . $singleEvents;


            Where it says start-min=".date("Y-m-d"), make that a parameter so you can do the separate calls.
              Jesse R.
              Consider trying something new and extraordinary.
              Illinois Wine

              Have you considered donating to MODx lately?
              Donate now. Every contribution helps.
              • 14050
              • 788 Posts
              This has been updated to use Simplepie 1.0.1. It should now be international friendly as it doesn’t parse the feed by relying on English words. Instead it uses some new Simplepie methods to look for certain items. It should be more efficient because of the new methods it relies on from Simplepie.

              http://modxcms.com/Google-Events-1.4.0-1710.html

              Nice new features include:

              Multiple Calendars can be mashed together into one.
              Limited Recurring Event Support

              Now that I have switched to SimplePie 1.0.1, I think I can have better support for recurring events. How do most people plan on listing recurring events? Right now it still uses the singleevent parameter so recurring events appear as single events. Please provide some ideas on how you would like recurring events to be displayed if that parameter was set to false.
                Jesse R.
                Consider trying something new and extraordinary.
                Illinois Wine

                Have you considered donating to MODx lately?
                Donate now. Every contribution helps.
                • 14050
                • 788 Posts
                It has been updated once more. I fixed the dependency on the offset which would have made it difficult to truly mash calendars together.

                Current version is 1.4.1 and is in the repository.
                  Jesse R.
                  Consider trying something new and extraordinary.
                  Illinois Wine

                  Have you considered donating to MODx lately?
                  Donate now. Every contribution helps.
                  • 24731 ☆ A M B ☆
                  • 211 Posts
                  Quote from: Jesse at Nov 19, 2007, 01:03 PM

                  It has been updated once more. I fixed the dependency on the offset which would have made it difficult to truly mash calendars together.

                  Current version is 1.4.1 and is in the repository.

                  Great Job, I have not yet updated but will do so during the weekend. I believe the best approach on recurring events is to have just one display for it covering its entire life time, at least it is my most useful way of seeing it.

                  Like right now I have a calendar for which I display theater plays. They exist for a month or so. I have 5 different plays and each spans through 30 days.

                  I don’t need to show each day individually unless I make a specific call for today for it where it only comes up once for today. Instead I just have an entry that covers the entire lifetime for the event and will exist for as long as the current date is within the lifetime for the event.

                  Not sure if that was clear enough but I hope it was.

                  Regards,
                  Jose R. Lopez
                    • 14050
                    • 788 Posts
                    @redstormj. Let me see if I can get this correct by way of example.

                    These are the events:

                    Today will be 11/20/07
                    Event A - 11/20/07
                    Event B - 11/30/07
                    Event C - 12/05/07
                    Event R - Recurs Daily starting on 11/20/07 and recurs until 12/05/07

                    Call Googleevents to show events for next 30 days.

                    On 11/20/07 it would display:
                    Event A - 11/20/07
                    Event R - 11/20/07
                    Event B - 11/30/07
                    Event C - 11/05/07

                    On 11/21/07 it would display:
                    Event R - 11/21/07 (Since it recurs on this day).
                    Event B - 11/30/07
                    Event C - 11/05/07

                    If that is the case, I think the code is pretty close to that already, I just have to find away to sort the recurrence away appropriately.
                      Jesse R.
                      Consider trying something new and extraordinary.
                      Illinois Wine

                      Have you considered donating to MODx lately?
                      Donate now. Every contribution helps.
                      • 14050
                      • 788 Posts
                      Just to let everyone know. I think I am going to bring some ajax functionality to this little snippet. Here is what I have so far:

                      http://www.cmssandbox.com/MODx095/index.php?id=89

                      It tosses a few javascript errors and looks ugly as all get out, but is still functional. However, it won’t allow multiple events on the same day with this package. It appears that some character or something inside a few of the descriptions for the events causes an error of "unterminated string literal" when trying to use the setHTML method in mootools. Any ideas? Also, is anyone going to find this functionality useful? Also, I am barely understand AJAX, let alone PHP and am kludging along with this calendar class:

                      http://evolt.org/node/60673

                      If anyone has a better one for me to use, let me know. As I start to play with AJAX more, maybe I will figure out how to do it with the PHP calendar class I was using previously.
                        Jesse R.
                        Consider trying something new and extraordinary.
                        Illinois Wine

                        Have you considered donating to MODx lately?
                        Donate now. Every contribution helps.